forked from TrueCloudLab/distribution
Merge pull request #4023 from DavidSpek/remove-schema1-manifest
feat!: remove schema1 manifest and config options
This commit is contained in:
commit
df16076366
14 changed files with 6 additions and 1484 deletions
|
@ -174,30 +174,6 @@ type Configuration struct {
|
||||||
|
|
||||||
Proxy Proxy `yaml:"proxy,omitempty"`
|
Proxy Proxy `yaml:"proxy,omitempty"`
|
||||||
|
|
||||||
// Compatibility is used for configurations of working with older or deprecated features.
|
|
||||||
Compatibility struct {
|
|
||||||
// Schema1 configures how schema1 manifests will be handled.
|
|
||||||
//
|
|
||||||
// Deprecated: Docker Image Manifest v2, Schema 1 is deprecated since
|
|
||||||
// 2015. These options should only be used if you need to provide
|
|
||||||
// backward compatibility.
|
|
||||||
Schema1 struct {
|
|
||||||
// TrustKey is the signing key to use for adding the signature to
|
|
||||||
// schema1 manifests.
|
|
||||||
//
|
|
||||||
// Deprecated: Docker Image Manifest v2, Schema 1 is deprecated since
|
|
||||||
// 2015. These options should only be used if you need to provide
|
|
||||||
// backward compatibility.
|
|
||||||
TrustKey string `yaml:"signingkeyfile,omitempty"`
|
|
||||||
// Enabled determines if schema1 manifests should be pullable.
|
|
||||||
//
|
|
||||||
// Deprecated: Docker Image Manifest v2, Schema 1 is deprecated since
|
|
||||||
// 2015. These options should only be used if you need to provide
|
|
||||||
// backward compatibility.
|
|
||||||
Enabled bool `yaml:"enabled,omitempty"`
|
|
||||||
} `yaml:"schema1,omitempty"`
|
|
||||||
} `yaml:"compatibility,omitempty"`
|
|
||||||
|
|
||||||
// Validation configures validation options for the registry.
|
// Validation configures validation options for the registry.
|
||||||
Validation struct {
|
Validation struct {
|
||||||
// Enabled enables the other options in this section. This field is
|
// Enabled enables the other options in this section. This field is
|
||||||
|
|
|
@ -276,10 +276,6 @@ proxy:
|
||||||
username: [username]
|
username: [username]
|
||||||
password: [password]
|
password: [password]
|
||||||
ttl: 168h
|
ttl: 168h
|
||||||
compatibility:
|
|
||||||
schema1:
|
|
||||||
signingkeyfile: /etc/registry/key.json
|
|
||||||
enabled: true
|
|
||||||
validation:
|
validation:
|
||||||
manifests:
|
manifests:
|
||||||
urls:
|
urls:
|
||||||
|
@ -1112,25 +1108,6 @@ username (such as `batman`) and the password for that username.
|
||||||
> **Note**: These private repositories are stored in the proxy cache's storage.
|
> **Note**: These private repositories are stored in the proxy cache's storage.
|
||||||
> Take appropriate measures to protect access to the proxy cache.
|
> Take appropriate measures to protect access to the proxy cache.
|
||||||
|
|
||||||
## `compatibility`
|
|
||||||
|
|
||||||
```none
|
|
||||||
compatibility:
|
|
||||||
schema1:
|
|
||||||
signingkeyfile: /etc/registry/key.json
|
|
||||||
enabled: true
|
|
||||||
```
|
|
||||||
|
|
||||||
Use the `compatibility` structure to configure handling of older and deprecated
|
|
||||||
features. Each subsection defines such a feature with configurable behavior.
|
|
||||||
|
|
||||||
### `schema1`
|
|
||||||
|
|
||||||
| Parameter | Required | Description |
|
|
||||||
|-----------|----------|-------------------------------------------------------|
|
|
||||||
| `signingkeyfile` | no | The signing private key used to add signatures to `schema1` manifests. If no signing key is provided, a new ECDSA key is generated when the registry starts. |
|
|
||||||
| `enabled` | no | If this is not set to true, `schema1` manifests cannot be pushed. |
|
|
||||||
|
|
||||||
## `validation`
|
## `validation`
|
||||||
|
|
||||||
```none
|
```none
|
||||||
|
|
|
@ -152,16 +152,16 @@ group unrelated events and send them in the same envelope to reduce the total
|
||||||
number of requests.
|
number of requests.
|
||||||
|
|
||||||
The full package has the mediatype
|
The full package has the mediatype
|
||||||
"application/vnd.docker.distribution.events.v1+json", which is set on the
|
"application/vnd.docker.distribution.events.v2+json", which is set on the
|
||||||
request coming to an endpoint.
|
request coming to an endpoint.
|
||||||
|
|
||||||
An example of a full event may look as follows:
|
An example of a full event may look as follows:
|
||||||
|
|
||||||
```http request
|
```http request
|
||||||
POST /callback HTTP/1.1
|
POST /callback HTTP/1.1
|
||||||
Host: application/vnd.docker.distribution.events.v1+json
|
Host: application/vnd.docker.distribution.events.v2+json
|
||||||
Authorization: Bearer <your token, if needed>
|
Authorization: Bearer <your token, if needed>
|
||||||
Content-Type: application/vnd.docker.distribution.events.v1+json
|
Content-Type: application/vnd.docker.distribution.events.v2+json
|
||||||
|
|
||||||
{
|
{
|
||||||
"events": [
|
"events": [
|
||||||
|
@ -170,7 +170,7 @@ Content-Type: application/vnd.docker.distribution.events.v1+json
|
||||||
"timestamp": "2006-01-02T15:04:05Z",
|
"timestamp": "2006-01-02T15:04:05Z",
|
||||||
"action": "push",
|
"action": "push",
|
||||||
"target": {
|
"target": {
|
||||||
"mediaType": "application/vnd.docker.distribution.manifest.v1+json",
|
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
|
||||||
"digest": "sha256:fea8895f450959fa676bcc1df0611ea93823a735a01205fd8622846041d0c7cf",
|
"digest": "sha256:fea8895f450959fa676bcc1df0611ea93823a735a01205fd8622846041d0c7cf",
|
||||||
"length": 1,
|
"length": 1,
|
||||||
"repository": "library/test",
|
"repository": "library/test",
|
||||||
|
|
|
@ -1,170 +0,0 @@
|
||||||
---
|
|
||||||
title: "Image Manifest V 2, Schema 1 "
|
|
||||||
description: "image manifest for the Registry."
|
|
||||||
keywords: registry, on-prem, images, tags, repository, distribution, api, advanced, manifest
|
|
||||||
---
|
|
||||||
|
|
||||||
# Image Manifest Version 2, Schema 1
|
|
||||||
|
|
||||||
> **Deprecated**
|
|
||||||
>
|
|
||||||
> Image Manifest v2, Schema 1 is deprecated. Use [Image Manifest v2, Schema 2](manifest-v2-2.md),
|
|
||||||
> or the [OCI Image Specification](https://github.com/opencontainers/image-spec).
|
|
||||||
> Image Manifest v2, Schema 1 should not be used for purposes other than backward
|
|
||||||
> compatibility.
|
|
||||||
|
|
||||||
This document outlines the format of the V2 image manifest. The image
|
|
||||||
manifest described herein was introduced in the Docker daemon in the [v1.3.0
|
|
||||||
release](https://github.com/docker/docker/commit/9f482a66ab37ec396ac61ed0c00d59122ac07453).
|
|
||||||
It is a provisional manifest to provide a compatibility with the [V1 Image
|
|
||||||
format](https://github.com/docker/docker/blob/master/image/spec/v1.md), as the
|
|
||||||
requirements are defined for the [V2 Schema 2
|
|
||||||
image](https://github.com/distribution/distribution/pull/62).
|
|
||||||
|
|
||||||
|
|
||||||
Image manifests describe the various constituents of a docker image. Image
|
|
||||||
manifests can be serialized to JSON format with the following media types:
|
|
||||||
|
|
||||||
Manifest Type | Media Type
|
|
||||||
------------- | -------------
|
|
||||||
manifest | "application/vnd.docker.distribution.manifest.v1+json"
|
|
||||||
signed manifest | "application/vnd.docker.distribution.manifest.v1+prettyjws"
|
|
||||||
|
|
||||||
*Note that "application/json" will also be accepted for schema 1.*
|
|
||||||
|
|
||||||
References:
|
|
||||||
|
|
||||||
- [Proposal: JSON Registry API V2.1](https://github.com/docker/docker/issues/9015)
|
|
||||||
- [Proposal: Provenance step 1 - Transform images for validation and verification](https://github.com/docker/docker/issues/8093)
|
|
||||||
|
|
||||||
## *Manifest* Field Descriptions
|
|
||||||
|
|
||||||
Manifest provides the base accessible fields for working with V2 image format
|
|
||||||
in the registry.
|
|
||||||
|
|
||||||
- **`name`** *string*
|
|
||||||
|
|
||||||
name is the name of the image's repository
|
|
||||||
|
|
||||||
- **`tag`** *string*
|
|
||||||
|
|
||||||
tag is the tag of the image
|
|
||||||
|
|
||||||
- **`architecture`** *string*
|
|
||||||
|
|
||||||
architecture is the host architecture on which this image is intended to
|
|
||||||
run. This is for information purposes and not currently used by the engine
|
|
||||||
|
|
||||||
- **`fsLayers`** *array*
|
|
||||||
|
|
||||||
fsLayers is a list of filesystem layer blob sums contained in this image.
|
|
||||||
|
|
||||||
An fsLayer is a struct consisting of the following fields
|
|
||||||
- **`blobSum`** *digest.Digest*
|
|
||||||
|
|
||||||
blobSum is the digest of the referenced filesystem image layer. A
|
|
||||||
digest must be a sha256 hash.
|
|
||||||
|
|
||||||
|
|
||||||
- **`history`** *array*
|
|
||||||
|
|
||||||
history is a list of unstructured historical data for v1 compatibility. It
|
|
||||||
contains ID of the image layer and ID of the layer's parent layers.
|
|
||||||
|
|
||||||
history is a struct consisting of the following fields
|
|
||||||
- **`v1Compatibility`** string
|
|
||||||
|
|
||||||
V1Compatibility is the raw V1 compatibility information. This will
|
|
||||||
contain the JSON object describing the V1 of this image.
|
|
||||||
|
|
||||||
- **`schemaVersion`** *int*
|
|
||||||
|
|
||||||
SchemaVersion is the image manifest schema that this image follows.
|
|
||||||
|
|
||||||
>**Note**:the length of `history` must be equal to the length of `fsLayers` and
|
|
||||||
>entries in each are correlated by index.
|
|
||||||
|
|
||||||
## Signed Manifests
|
|
||||||
|
|
||||||
Signed manifests provides an envelope for a signed image manifest. A signed
|
|
||||||
manifest consists of an image manifest along with an additional field
|
|
||||||
containing the signature of the manifest.
|
|
||||||
|
|
||||||
The docker client can verify signed manifests and displays a message to the user.
|
|
||||||
|
|
||||||
### Signing Manifests
|
|
||||||
|
|
||||||
Image manifests can be signed in two different ways: with a *libtrust* private
|
|
||||||
key or an x509 certificate chain. When signing with an x509 certificate chain,
|
|
||||||
the public key of the first element in the chain must be the public key
|
|
||||||
corresponding with the sign key.
|
|
||||||
|
|
||||||
### Signed Manifest Field Description
|
|
||||||
|
|
||||||
Signed manifests include an image manifest and a list of signatures generated
|
|
||||||
by *libtrust*. A signature consists of the following fields:
|
|
||||||
|
|
||||||
|
|
||||||
- **`header`** *[JOSE](http://tools.ietf.org/html/draft-ietf-jose-json-web-signature-31#section-2)*
|
|
||||||
|
|
||||||
A [JSON Web Signature](http://self-issued.info/docs/draft-ietf-jose-json-web-signature.html)
|
|
||||||
|
|
||||||
- **`signature`** *string*
|
|
||||||
|
|
||||||
A signature for the image manifest, signed by a *libtrust* private key
|
|
||||||
|
|
||||||
- **`protected`** *string*
|
|
||||||
|
|
||||||
The signed protected header
|
|
||||||
|
|
||||||
## Example Manifest
|
|
||||||
|
|
||||||
*Example showing the official 'hello-world' image manifest.*
|
|
||||||
|
|
||||||
```
|
|
||||||
{
|
|
||||||
"name": "hello-world",
|
|
||||||
"tag": "latest",
|
|
||||||
"architecture": "amd64",
|
|
||||||
"fsLayers": [
|
|
||||||
{
|
|
||||||
"blobSum": "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"blobSum": "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"blobSum": "sha256:cc8567d70002e957612902a8e985ea129d831ebe04057d88fb644857caa45d11"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"blobSum": "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"history": [
|
|
||||||
{
|
|
||||||
"v1Compatibility": "{\"id\":\"e45a5af57b00862e5ef5782a9925979a02ba2b12dff832fd0991335f4a11e5c5\",\"parent\":\"31cbccb51277105ba3ae35ce33c22b69c9e3f1002e76e4c736a2e8ebff9d7b5d\",\"created\":\"2014-12-31T22:57:59.178729048Z\",\"container\":\"27b45f8fb11795b52e9605b686159729b0d9ca92f76d40fb4f05a62e19c46b4f\",\"container_config\":{\"Hostname\":\"8ce6509d66e2\",\"Domainname\":\"\",\"User\":\"\",\"Memory\":0,\"MemorySwap\":0,\"CpuShares\":0,\"Cpuset\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\"],\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) CMD [/hello]\"],\"Image\":\"31cbccb51277105ba3ae35ce33c22b69c9e3f1002e76e4c736a2e8ebff9d7b5d\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":[],\"SecurityOpt\":null,\"Labels\":null},\"docker_version\":\"1.4.1\",\"config\":{\"Hostname\":\"8ce6509d66e2\",\"Domainname\":\"\",\"User\":\"\",\"Memory\":0,\"MemorySwap\":0,\"CpuShares\":0,\"Cpuset\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\"],\"Cmd\":[\"/hello\"],\"Image\":\"31cbccb51277105ba3ae35ce33c22b69c9e3f1002e76e4c736a2e8ebff9d7b5d\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":[],\"SecurityOpt\":null,\"Labels\":null},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":0}\n"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"v1Compatibility": "{\"id\":\"e45a5af57b00862e5ef5782a9925979a02ba2b12dff832fd0991335f4a11e5c5\",\"parent\":\"31cbccb51277105ba3ae35ce33c22b69c9e3f1002e76e4c736a2e8ebff9d7b5d\",\"created\":\"2014-12-31T22:57:59.178729048Z\",\"container\":\"27b45f8fb11795b52e9605b686159729b0d9ca92f76d40fb4f05a62e19c46b4f\",\"container_config\":{\"Hostname\":\"8ce6509d66e2\",\"Domainname\":\"\",\"User\":\"\",\"Memory\":0,\"MemorySwap\":0,\"CpuShares\":0,\"Cpuset\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\"],\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) CMD [/hello]\"],\"Image\":\"31cbccb51277105ba3ae35ce33c22b69c9e3f1002e76e4c736a2e8ebff9d7b5d\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":[],\"SecurityOpt\":null,\"Labels\":null},\"docker_version\":\"1.4.1\",\"config\":{\"Hostname\":\"8ce6509d66e2\",\"Domainname\":\"\",\"User\":\"\",\"Memory\":0,\"MemorySwap\":0,\"CpuShares\":0,\"Cpuset\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\"],\"Cmd\":[\"/hello\"],\"Image\":\"31cbccb51277105ba3ae35ce33c22b69c9e3f1002e76e4c736a2e8ebff9d7b5d\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":[],\"SecurityOpt\":null,\"Labels\":null},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":0}\n"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"schemaVersion": 1,
|
|
||||||
"signatures": [
|
|
||||||
{
|
|
||||||
"header": {
|
|
||||||
"jwk": {
|
|
||||||
"crv": "P-256",
|
|
||||||
"kid": "OD6I:6DRK:JXEJ:KBM4:255X:NSAA:MUSF:E4VM:ZI6W:CUN2:L4Z6:LSF4",
|
|
||||||
"kty": "EC",
|
|
||||||
"x": "3gAwX48IQ5oaYQAYSxor6rYYc_6yjuLCjtQ9LUakg4A",
|
|
||||||
"y": "t72ge6kIA1XOjqjVoEOiPPAURltJFBMGDSQvEGVB010"
|
|
||||||
},
|
|
||||||
"alg": "ES256"
|
|
||||||
},
|
|
||||||
"signature": "XREm0L8WNn27Ga_iE_vRnTxVMhhYY0Zst_FfkKopg6gWSoTOZTuW4rK0fg_IqnKkEKlbD83tD46LKEGi5aIVFg",
|
|
||||||
"protected": "eyJmb3JtYXRMZW5ndGgiOjY2MjgsImZvcm1hdFRhaWwiOiJDbjAiLCJ0aW1lIjoiMjAxNS0wNC0wOFQxODo1Mjo1OVoifQ"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
```
|
|
|
@ -24,7 +24,6 @@ an ID for the image.
|
||||||
The following media types are used by the manifest formats described here, and
|
The following media types are used by the manifest formats described here, and
|
||||||
the resources they reference:
|
the resources they reference:
|
||||||
|
|
||||||
- `application/vnd.docker.distribution.manifest.v1+json`: schema1 (existing manifest format)
|
|
||||||
- `application/vnd.docker.distribution.manifest.v2+json`: New image manifest format (schemaVersion = 2)
|
- `application/vnd.docker.distribution.manifest.v2+json`: New image manifest format (schemaVersion = 2)
|
||||||
- `application/vnd.docker.distribution.manifest.list.v2+json`: Manifest list, aka "fat manifest"
|
- `application/vnd.docker.distribution.manifest.list.v2+json`: Manifest list, aka "fat manifest"
|
||||||
- `application/vnd.docker.container.image.v1+json`: Container config JSON
|
- `application/vnd.docker.container.image.v1+json`: Container config JSON
|
||||||
|
@ -60,9 +59,7 @@ image manifest based on the Content-Type returned in the HTTP response.
|
||||||
- **`mediaType`** *string*
|
- **`mediaType`** *string*
|
||||||
|
|
||||||
The MIME type of the referenced object. This will generally be
|
The MIME type of the referenced object. This will generally be
|
||||||
`application/vnd.docker.distribution.manifest.v2+json`, but it could also
|
`application/vnd.docker.distribution.manifest.v2+json`.
|
||||||
be `application/vnd.docker.distribution.manifest.v1+json` if the manifest
|
|
||||||
list references a legacy schema-1 manifest.
|
|
||||||
|
|
||||||
- **`size`** *int*
|
- **`size`** *int*
|
||||||
|
|
||||||
|
|
|
@ -1,303 +0,0 @@
|
||||||
package schema1
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto/sha512"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/distribution/distribution/v3"
|
|
||||||
"github.com/distribution/distribution/v3/manifest"
|
|
||||||
"github.com/distribution/distribution/v3/reference"
|
|
||||||
"github.com/docker/libtrust"
|
|
||||||
"github.com/opencontainers/go-digest"
|
|
||||||
)
|
|
||||||
|
|
||||||
type diffID digest.Digest
|
|
||||||
|
|
||||||
// gzippedEmptyTar is a gzip-compressed version of an empty tar file
|
|
||||||
// (1024 NULL bytes)
|
|
||||||
var gzippedEmptyTar = []byte{
|
|
||||||
31, 139, 8, 0, 0, 9, 110, 136, 0, 255, 98, 24, 5, 163, 96, 20, 140, 88,
|
|
||||||
0, 8, 0, 0, 255, 255, 46, 175, 181, 239, 0, 4, 0, 0,
|
|
||||||
}
|
|
||||||
|
|
||||||
// digestSHA256GzippedEmptyTar is the canonical sha256 digest of
|
|
||||||
// gzippedEmptyTar
|
|
||||||
const digestSHA256GzippedEmptyTar = digest.Digest("sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4")
|
|
||||||
|
|
||||||
// configManifestBuilder is a type for constructing manifests from an image
|
|
||||||
// configuration and generic descriptors.
|
|
||||||
type configManifestBuilder struct {
|
|
||||||
// bs is a BlobService used to create empty layer tars in the
|
|
||||||
// blob store if necessary.
|
|
||||||
bs distribution.BlobService
|
|
||||||
// pk is the libtrust private key used to sign the final manifest.
|
|
||||||
pk libtrust.PrivateKey
|
|
||||||
// configJSON is configuration supplied when the ManifestBuilder was
|
|
||||||
// created.
|
|
||||||
configJSON []byte
|
|
||||||
// ref contains the name and optional tag provided to NewConfigManifestBuilder.
|
|
||||||
ref reference.Named
|
|
||||||
// descriptors is the set of descriptors referencing the layers.
|
|
||||||
descriptors []distribution.Descriptor
|
|
||||||
// emptyTarDigest is set to a valid digest if an empty tar has been
|
|
||||||
// put in the blob store; otherwise it is empty.
|
|
||||||
emptyTarDigest digest.Digest
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewConfigManifestBuilder is used to build new manifests for the current
|
|
||||||
// schema version from an image configuration and a set of descriptors.
|
|
||||||
// It takes a BlobService so that it can add an empty tar to the blob store
|
|
||||||
// if the resulting manifest needs empty layers.
|
|
||||||
//
|
|
||||||
// Deprecated: Docker Image Manifest v2, Schema 1 is deprecated since 2015,
|
|
||||||
// use Docker Image Manifest v2, Schema 2, or the OCI Image Specification v1.
|
|
||||||
// This package should not be used for purposes other than backward compatibility.
|
|
||||||
func NewConfigManifestBuilder(bs distribution.BlobService, pk libtrust.PrivateKey, ref reference.Named, configJSON []byte) distribution.ManifestBuilder {
|
|
||||||
return &configManifestBuilder{
|
|
||||||
bs: bs,
|
|
||||||
pk: pk,
|
|
||||||
configJSON: configJSON,
|
|
||||||
ref: ref,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build produces a final manifest from the given references.
|
|
||||||
//
|
|
||||||
// Deprecated: Docker Image Manifest v2, Schema 1 is deprecated since 2015.
|
|
||||||
// Use Docker Image Manifest v2, Schema 2, or the OCI Image Specification.
|
|
||||||
func (mb *configManifestBuilder) Build(ctx context.Context) (m distribution.Manifest, err error) {
|
|
||||||
type imageRootFS struct {
|
|
||||||
Type string `json:"type"`
|
|
||||||
DiffIDs []diffID `json:"diff_ids,omitempty"`
|
|
||||||
BaseLayer string `json:"base_layer,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type imageHistory struct {
|
|
||||||
Created time.Time `json:"created"`
|
|
||||||
Author string `json:"author,omitempty"`
|
|
||||||
CreatedBy string `json:"created_by,omitempty"`
|
|
||||||
Comment string `json:"comment,omitempty"`
|
|
||||||
EmptyLayer bool `json:"empty_layer,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type imageConfig struct {
|
|
||||||
RootFS *imageRootFS `json:"rootfs,omitempty"`
|
|
||||||
History []imageHistory `json:"history,omitempty"`
|
|
||||||
Architecture string `json:"architecture,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var img imageConfig
|
|
||||||
|
|
||||||
if err := json.Unmarshal(mb.configJSON, &img); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(img.History) == 0 {
|
|
||||||
return nil, errors.New("empty history when trying to create schema1 manifest")
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(img.RootFS.DiffIDs) != len(mb.descriptors) {
|
|
||||||
return nil, fmt.Errorf("number of descriptors and number of layers in rootfs must match: len(%v) != len(%v)", img.RootFS.DiffIDs, mb.descriptors)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate IDs for each layer
|
|
||||||
// For non-top-level layers, create fake V1Compatibility strings that
|
|
||||||
// fit the format and don't collide with anything else, but don't
|
|
||||||
// result in runnable images on their own.
|
|
||||||
type v1Compatibility struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
Parent string `json:"parent,omitempty"`
|
|
||||||
Comment string `json:"comment,omitempty"`
|
|
||||||
Created time.Time `json:"created"`
|
|
||||||
ContainerConfig struct {
|
|
||||||
Cmd []string
|
|
||||||
} `json:"container_config,omitempty"`
|
|
||||||
Author string `json:"author,omitempty"`
|
|
||||||
ThrowAway bool `json:"throwaway,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
fsLayerList := make([]FSLayer, len(img.History))
|
|
||||||
history := make([]History, len(img.History))
|
|
||||||
|
|
||||||
parent := ""
|
|
||||||
layerCounter := 0
|
|
||||||
for i, h := range img.History[:len(img.History)-1] {
|
|
||||||
var blobsum digest.Digest
|
|
||||||
if h.EmptyLayer {
|
|
||||||
if blobsum, err = mb.emptyTar(ctx); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if len(img.RootFS.DiffIDs) <= layerCounter {
|
|
||||||
return nil, errors.New("too many non-empty layers in History section")
|
|
||||||
}
|
|
||||||
blobsum = mb.descriptors[layerCounter].Digest
|
|
||||||
layerCounter++
|
|
||||||
}
|
|
||||||
|
|
||||||
v1ID := digest.FromBytes([]byte(blobsum.Encoded() + " " + parent)).Encoded()
|
|
||||||
|
|
||||||
if i == 0 && img.RootFS.BaseLayer != "" {
|
|
||||||
// windows-only baselayer setup
|
|
||||||
baseID := sha512.Sum384([]byte(img.RootFS.BaseLayer))
|
|
||||||
parent = fmt.Sprintf("%x", baseID[:32])
|
|
||||||
}
|
|
||||||
|
|
||||||
v1Compat := v1Compatibility{
|
|
||||||
ID: v1ID,
|
|
||||||
Parent: parent,
|
|
||||||
Comment: h.Comment,
|
|
||||||
Created: h.Created,
|
|
||||||
Author: h.Author,
|
|
||||||
}
|
|
||||||
v1Compat.ContainerConfig.Cmd = []string{img.History[i].CreatedBy}
|
|
||||||
if h.EmptyLayer {
|
|
||||||
v1Compat.ThrowAway = true
|
|
||||||
}
|
|
||||||
jsonBytes, err := json.Marshal(&v1Compat)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
reversedIndex := len(img.History) - i - 1
|
|
||||||
history[reversedIndex].V1Compatibility = string(jsonBytes)
|
|
||||||
fsLayerList[reversedIndex] = FSLayer{BlobSum: blobsum}
|
|
||||||
|
|
||||||
parent = v1ID
|
|
||||||
}
|
|
||||||
|
|
||||||
latestHistory := img.History[len(img.History)-1]
|
|
||||||
|
|
||||||
var blobsum digest.Digest
|
|
||||||
if latestHistory.EmptyLayer {
|
|
||||||
if blobsum, err = mb.emptyTar(ctx); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if len(img.RootFS.DiffIDs) <= layerCounter {
|
|
||||||
return nil, errors.New("too many non-empty layers in History section")
|
|
||||||
}
|
|
||||||
blobsum = mb.descriptors[layerCounter].Digest
|
|
||||||
}
|
|
||||||
|
|
||||||
fsLayerList[0] = FSLayer{BlobSum: blobsum}
|
|
||||||
dgst := digest.FromBytes([]byte(blobsum.Encoded() + " " + parent + " " + string(mb.configJSON)))
|
|
||||||
|
|
||||||
// Top-level v1compatibility string should be a modified version of the
|
|
||||||
// image config.
|
|
||||||
transformedConfig, err := MakeV1ConfigFromConfig(mb.configJSON, dgst.Encoded(), parent, latestHistory.EmptyLayer)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
history[0].V1Compatibility = string(transformedConfig)
|
|
||||||
|
|
||||||
tag := ""
|
|
||||||
if tagged, isTagged := mb.ref.(reference.Tagged); isTagged {
|
|
||||||
tag = tagged.Tag()
|
|
||||||
}
|
|
||||||
|
|
||||||
mfst := Manifest{
|
|
||||||
Versioned: manifest.Versioned{
|
|
||||||
SchemaVersion: 1,
|
|
||||||
},
|
|
||||||
Name: mb.ref.Name(),
|
|
||||||
Tag: tag,
|
|
||||||
Architecture: img.Architecture,
|
|
||||||
FSLayers: fsLayerList,
|
|
||||||
History: history,
|
|
||||||
}
|
|
||||||
|
|
||||||
return Sign(&mfst, mb.pk)
|
|
||||||
}
|
|
||||||
|
|
||||||
// emptyTar pushes a compressed empty tar to the blob store if one doesn't
|
|
||||||
// already exist, and returns its blobsum.
|
|
||||||
func (mb *configManifestBuilder) emptyTar(ctx context.Context) (digest.Digest, error) {
|
|
||||||
if mb.emptyTarDigest != "" {
|
|
||||||
// Already put an empty tar
|
|
||||||
return mb.emptyTarDigest, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
descriptor, err := mb.bs.Stat(ctx, digestSHA256GzippedEmptyTar)
|
|
||||||
switch err {
|
|
||||||
case nil:
|
|
||||||
mb.emptyTarDigest = descriptor.Digest
|
|
||||||
return descriptor.Digest, nil
|
|
||||||
case distribution.ErrBlobUnknown:
|
|
||||||
// nop
|
|
||||||
default:
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add gzipped empty tar to the blob store
|
|
||||||
descriptor, err = mb.bs.Put(ctx, "", gzippedEmptyTar)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
mb.emptyTarDigest = descriptor.Digest
|
|
||||||
|
|
||||||
return descriptor.Digest, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// AppendReference adds a reference to the current ManifestBuilder.
|
|
||||||
//
|
|
||||||
// Deprecated: Docker Image Manifest v2, Schema 1 is deprecated since 2015.
|
|
||||||
// Use Docker Image Manifest v2, Schema 2, or the OCI Image Specification.
|
|
||||||
func (mb *configManifestBuilder) AppendReference(d distribution.Describable) error {
|
|
||||||
descriptor := d.Descriptor()
|
|
||||||
|
|
||||||
if err := descriptor.Digest.Validate(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
mb.descriptors = append(mb.descriptors, descriptor)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// References returns the current references added to this builder.
|
|
||||||
//
|
|
||||||
// Deprecated: Docker Image Manifest v2, Schema 1 is deprecated since 2015.
|
|
||||||
// Use Docker Image Manifest v2, Schema 2, or the OCI Image Specification.
|
|
||||||
func (mb *configManifestBuilder) References() []distribution.Descriptor {
|
|
||||||
return mb.descriptors
|
|
||||||
}
|
|
||||||
|
|
||||||
// MakeV1ConfigFromConfig creates an legacy V1 image config from image config JSON.
|
|
||||||
//
|
|
||||||
// Deprecated: Docker Image Manifest v2, Schema 1 is deprecated since 2015.
|
|
||||||
// Use Docker Image Manifest v2, Schema 2, or the OCI Image Specification.
|
|
||||||
func MakeV1ConfigFromConfig(configJSON []byte, v1ID, parentV1ID string, throwaway bool) ([]byte, error) {
|
|
||||||
// Top-level v1compatibility string should be a modified version of the
|
|
||||||
// image config.
|
|
||||||
var configAsMap map[string]*json.RawMessage
|
|
||||||
if err := json.Unmarshal(configJSON, &configAsMap); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete fields that didn't exist in old manifest
|
|
||||||
delete(configAsMap, "rootfs")
|
|
||||||
delete(configAsMap, "history")
|
|
||||||
configAsMap["id"] = rawJSON(v1ID)
|
|
||||||
if parentV1ID != "" {
|
|
||||||
configAsMap["parent"] = rawJSON(parentV1ID)
|
|
||||||
}
|
|
||||||
if throwaway {
|
|
||||||
configAsMap["throwaway"] = rawJSON(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
return json.Marshal(configAsMap)
|
|
||||||
}
|
|
||||||
|
|
||||||
func rawJSON(value interface{}) *json.RawMessage {
|
|
||||||
jsonval, err := json.Marshal(value)
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return (*json.RawMessage)(&jsonval)
|
|
||||||
}
|
|
|
@ -1,262 +0,0 @@
|
||||||
package schema1
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"compress/gzip"
|
|
||||||
"context"
|
|
||||||
"io"
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/distribution/distribution/v3"
|
|
||||||
dcontext "github.com/distribution/distribution/v3/context"
|
|
||||||
"github.com/distribution/distribution/v3/reference"
|
|
||||||
"github.com/docker/libtrust"
|
|
||||||
"github.com/opencontainers/go-digest"
|
|
||||||
)
|
|
||||||
|
|
||||||
type mockBlobService struct {
|
|
||||||
descriptors map[digest.Digest]distribution.Descriptor
|
|
||||||
distribution.BlobService
|
|
||||||
}
|
|
||||||
|
|
||||||
func (bs *mockBlobService) Stat(ctx context.Context, dgst digest.Digest) (distribution.Descriptor, error) {
|
|
||||||
if descriptor, ok := bs.descriptors[dgst]; ok {
|
|
||||||
return descriptor, nil
|
|
||||||
}
|
|
||||||
return distribution.Descriptor{}, distribution.ErrBlobUnknown
|
|
||||||
}
|
|
||||||
|
|
||||||
func (bs *mockBlobService) Put(ctx context.Context, mediaType string, p []byte) (distribution.Descriptor, error) {
|
|
||||||
d := distribution.Descriptor{
|
|
||||||
MediaType: mediaType,
|
|
||||||
Digest: digest.FromBytes(p),
|
|
||||||
Size: int64(len(p)),
|
|
||||||
}
|
|
||||||
bs.descriptors[d.Digest] = d
|
|
||||||
return d, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEmptyTar(t *testing.T) {
|
|
||||||
// Confirm that gzippedEmptyTar expands to 1024 NULL bytes.
|
|
||||||
var decompressed [2048]byte
|
|
||||||
gzipReader, err := gzip.NewReader(bytes.NewReader(gzippedEmptyTar))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("NewReader returned error: %v", err)
|
|
||||||
}
|
|
||||||
n, _ := gzipReader.Read(decompressed[:])
|
|
||||||
if n != 1024 {
|
|
||||||
t.Fatalf("read returned %d bytes; expected 1024", n)
|
|
||||||
}
|
|
||||||
n, err = gzipReader.Read(decompressed[1024:])
|
|
||||||
if n != 0 {
|
|
||||||
t.Fatalf("read returned %d bytes; expected 0", n)
|
|
||||||
}
|
|
||||||
if err != io.EOF {
|
|
||||||
t.Fatal("read did not return io.EOF")
|
|
||||||
}
|
|
||||||
gzipReader.Close()
|
|
||||||
for _, b := range decompressed[:1024] {
|
|
||||||
if b != 0 {
|
|
||||||
t.Fatal("nonzero byte in decompressed tar")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Confirm that digestSHA256EmptyTar is the digest of gzippedEmptyTar.
|
|
||||||
dgst := digest.FromBytes(gzippedEmptyTar)
|
|
||||||
if dgst != digestSHA256GzippedEmptyTar {
|
|
||||||
t.Fatalf("digest mismatch for empty tar: expected %s got %s", digestSHA256GzippedEmptyTar, dgst)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConfigBuilder(t *testing.T) {
|
|
||||||
imgJSON := `{
|
|
||||||
"architecture": "amd64",
|
|
||||||
"config": {
|
|
||||||
"AttachStderr": false,
|
|
||||||
"AttachStdin": false,
|
|
||||||
"AttachStdout": false,
|
|
||||||
"Cmd": [
|
|
||||||
"/bin/sh",
|
|
||||||
"-c",
|
|
||||||
"echo hi"
|
|
||||||
],
|
|
||||||
"Domainname": "",
|
|
||||||
"Entrypoint": null,
|
|
||||||
"Env": [
|
|
||||||
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
|
|
||||||
"derived=true",
|
|
||||||
"asdf=true"
|
|
||||||
],
|
|
||||||
"Hostname": "23304fc829f9",
|
|
||||||
"Image": "sha256:4ab15c48b859c2920dd5224f92aabcd39a52794c5b3cf088fb3bbb438756c246",
|
|
||||||
"Labels": {},
|
|
||||||
"OnBuild": [],
|
|
||||||
"OpenStdin": false,
|
|
||||||
"StdinOnce": false,
|
|
||||||
"Tty": false,
|
|
||||||
"User": "",
|
|
||||||
"Volumes": null,
|
|
||||||
"WorkingDir": ""
|
|
||||||
},
|
|
||||||
"container": "e91032eb0403a61bfe085ff5a5a48e3659e5a6deae9f4d678daa2ae399d5a001",
|
|
||||||
"container_config": {
|
|
||||||
"AttachStderr": false,
|
|
||||||
"AttachStdin": false,
|
|
||||||
"AttachStdout": false,
|
|
||||||
"Cmd": [
|
|
||||||
"/bin/sh",
|
|
||||||
"-c",
|
|
||||||
"#(nop) CMD [\"/bin/sh\" \"-c\" \"echo hi\"]"
|
|
||||||
],
|
|
||||||
"Domainname": "",
|
|
||||||
"Entrypoint": null,
|
|
||||||
"Env": [
|
|
||||||
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
|
|
||||||
"derived=true",
|
|
||||||
"asdf=true"
|
|
||||||
],
|
|
||||||
"Hostname": "23304fc829f9",
|
|
||||||
"Image": "sha256:4ab15c48b859c2920dd5224f92aabcd39a52794c5b3cf088fb3bbb438756c246",
|
|
||||||
"Labels": {},
|
|
||||||
"OnBuild": [],
|
|
||||||
"OpenStdin": false,
|
|
||||||
"StdinOnce": false,
|
|
||||||
"Tty": false,
|
|
||||||
"User": "",
|
|
||||||
"Volumes": null,
|
|
||||||
"WorkingDir": ""
|
|
||||||
},
|
|
||||||
"created": "2015-11-04T23:06:32.365666163Z",
|
|
||||||
"docker_version": "1.9.0-dev",
|
|
||||||
"history": [
|
|
||||||
{
|
|
||||||
"created": "2015-10-31T22:22:54.690851953Z",
|
|
||||||
"created_by": "/bin/sh -c #(nop) ADD file:a3bc1e842b69636f9df5256c49c5374fb4eef1e281fe3f282c65fb853ee171c5 in /"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"created": "2015-10-31T22:22:55.613815829Z",
|
|
||||||
"created_by": "/bin/sh -c #(nop) CMD [\"sh\"]"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"created": "2015-11-04T23:06:30.934316144Z",
|
|
||||||
"created_by": "/bin/sh -c #(nop) ENV derived=true",
|
|
||||||
"empty_layer": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"created": "2015-11-04T23:06:31.192097572Z",
|
|
||||||
"created_by": "/bin/sh -c #(nop) ENV asdf=true",
|
|
||||||
"empty_layer": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"author": "Alyssa P. Hacker \u003calyspdev@example.com\u003e",
|
|
||||||
"created": "2015-11-04T23:06:32.083868454Z",
|
|
||||||
"created_by": "/bin/sh -c dd if=/dev/zero of=/file bs=1024 count=1024"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"created": "2015-11-04T23:06:32.365666163Z",
|
|
||||||
"created_by": "/bin/sh -c #(nop) CMD [\"/bin/sh\" \"-c\" \"echo hi\"]",
|
|
||||||
"empty_layer": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"os": "linux",
|
|
||||||
"rootfs": {
|
|
||||||
"diff_ids": [
|
|
||||||
"sha256:c6f988f4874bb0add23a778f753c65efe992244e148a1d2ec2a8b664fb66bbd1",
|
|
||||||
"sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef",
|
|
||||||
"sha256:13f53e08df5a220ab6d13c58b2bf83a59cbdc2e04d0a3f041ddf4b0ba4112d49"
|
|
||||||
],
|
|
||||||
"type": "layers"
|
|
||||||
}
|
|
||||||
}`
|
|
||||||
|
|
||||||
descriptors := []distribution.Descriptor{
|
|
||||||
{Digest: digest.Digest("sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4")},
|
|
||||||
{Digest: digest.Digest("sha256:86e0e091d0da6bde2456dbb48306f3956bbeb2eae1b5b9a43045843f69fe4aaa")},
|
|
||||||
{Digest: digest.Digest("sha256:b4ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4")},
|
|
||||||
}
|
|
||||||
|
|
||||||
pk, err := libtrust.GenerateECP256PrivateKey()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("could not generate key for testing: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
bs := &mockBlobService{descriptors: make(map[digest.Digest]distribution.Descriptor)}
|
|
||||||
|
|
||||||
ref, err := reference.WithName("testrepo")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("could not parse reference: %v", err)
|
|
||||||
}
|
|
||||||
ref, err = reference.WithTag(ref, "testtag")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("could not add tag: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
builder := NewConfigManifestBuilder(bs, pk, ref, []byte(imgJSON))
|
|
||||||
|
|
||||||
for _, d := range descriptors {
|
|
||||||
if err := builder.AppendReference(d); err != nil {
|
|
||||||
t.Fatalf("AppendReference returned error: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
signed, err := builder.Build(dcontext.Background())
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Build returned error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check that the gzipped empty layer tar was put in the blob store
|
|
||||||
_, err = bs.Stat(dcontext.Background(), digestSHA256GzippedEmptyTar)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("gzipped empty tar was not put in the blob store")
|
|
||||||
}
|
|
||||||
|
|
||||||
manifest := signed.(*SignedManifest).Manifest
|
|
||||||
|
|
||||||
if manifest.Versioned.SchemaVersion != 1 {
|
|
||||||
t.Fatal("SchemaVersion != 1")
|
|
||||||
}
|
|
||||||
if manifest.Name != "testrepo" {
|
|
||||||
t.Fatal("incorrect name in manifest")
|
|
||||||
}
|
|
||||||
if manifest.Tag != "testtag" {
|
|
||||||
t.Fatal("incorrect tag in manifest")
|
|
||||||
}
|
|
||||||
if manifest.Architecture != "amd64" {
|
|
||||||
t.Fatal("incorrect arch in manifest")
|
|
||||||
}
|
|
||||||
|
|
||||||
expectedFSLayers := []FSLayer{
|
|
||||||
{BlobSum: digest.Digest("sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4")},
|
|
||||||
{BlobSum: digest.Digest("sha256:b4ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4")},
|
|
||||||
{BlobSum: digest.Digest("sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4")},
|
|
||||||
{BlobSum: digest.Digest("sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4")},
|
|
||||||
{BlobSum: digest.Digest("sha256:86e0e091d0da6bde2456dbb48306f3956bbeb2eae1b5b9a43045843f69fe4aaa")},
|
|
||||||
{BlobSum: digest.Digest("sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4")},
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(manifest.FSLayers) != len(expectedFSLayers) {
|
|
||||||
t.Fatalf("wrong number of FSLayers: %d", len(manifest.FSLayers))
|
|
||||||
}
|
|
||||||
if !reflect.DeepEqual(manifest.FSLayers, expectedFSLayers) {
|
|
||||||
t.Fatal("wrong FSLayers list")
|
|
||||||
}
|
|
||||||
|
|
||||||
expectedV1Compatibility := []string{
|
|
||||||
`{"architecture":"amd64","config":{"AttachStderr":false,"AttachStdin":false,"AttachStdout":false,"Cmd":["/bin/sh","-c","echo hi"],"Domainname":"","Entrypoint":null,"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin","derived=true","asdf=true"],"Hostname":"23304fc829f9","Image":"sha256:4ab15c48b859c2920dd5224f92aabcd39a52794c5b3cf088fb3bbb438756c246","Labels":{},"OnBuild":[],"OpenStdin":false,"StdinOnce":false,"Tty":false,"User":"","Volumes":null,"WorkingDir":""},"container":"e91032eb0403a61bfe085ff5a5a48e3659e5a6deae9f4d678daa2ae399d5a001","container_config":{"AttachStderr":false,"AttachStdin":false,"AttachStdout":false,"Cmd":["/bin/sh","-c","#(nop) CMD [\"/bin/sh\" \"-c\" \"echo hi\"]"],"Domainname":"","Entrypoint":null,"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin","derived=true","asdf=true"],"Hostname":"23304fc829f9","Image":"sha256:4ab15c48b859c2920dd5224f92aabcd39a52794c5b3cf088fb3bbb438756c246","Labels":{},"OnBuild":[],"OpenStdin":false,"StdinOnce":false,"Tty":false,"User":"","Volumes":null,"WorkingDir":""},"created":"2015-11-04T23:06:32.365666163Z","docker_version":"1.9.0-dev","id":"69e5c1bfadad697fdb6db59f6326648fa119e0c031a0eda33b8cfadcab54ba7f","os":"linux","parent":"74cf9c92699240efdba1903c2748ef57105d5bedc588084c4e88f3bb1c3ef0b0","throwaway":true}`,
|
|
||||||
`{"id":"74cf9c92699240efdba1903c2748ef57105d5bedc588084c4e88f3bb1c3ef0b0","parent":"178be37afc7c49e951abd75525dbe0871b62ad49402f037164ee6314f754599d","created":"2015-11-04T23:06:32.083868454Z","container_config":{"Cmd":["/bin/sh -c dd if=/dev/zero of=/file bs=1024 count=1024"]},"author":"Alyssa P. Hacker \u003calyspdev@example.com\u003e"}`,
|
|
||||||
`{"id":"178be37afc7c49e951abd75525dbe0871b62ad49402f037164ee6314f754599d","parent":"b449305a55a283538c4574856a8b701f2a3d5ec08ef8aec47f385f20339a4866","created":"2015-11-04T23:06:31.192097572Z","container_config":{"Cmd":["/bin/sh -c #(nop) ENV asdf=true"]},"throwaway":true}`,
|
|
||||||
`{"id":"b449305a55a283538c4574856a8b701f2a3d5ec08ef8aec47f385f20339a4866","parent":"9e3447ca24cb96d86ebd5960cb34d1299b07e0a0e03801d90b9969a2c187dd6e","created":"2015-11-04T23:06:30.934316144Z","container_config":{"Cmd":["/bin/sh -c #(nop) ENV derived=true"]},"throwaway":true}`,
|
|
||||||
`{"id":"9e3447ca24cb96d86ebd5960cb34d1299b07e0a0e03801d90b9969a2c187dd6e","parent":"3690474eb5b4b26fdfbd89c6e159e8cc376ca76ef48032a30fa6aafd56337880","created":"2015-10-31T22:22:55.613815829Z","container_config":{"Cmd":["/bin/sh -c #(nop) CMD [\"sh\"]"]}}`,
|
|
||||||
`{"id":"3690474eb5b4b26fdfbd89c6e159e8cc376ca76ef48032a30fa6aafd56337880","created":"2015-10-31T22:22:54.690851953Z","container_config":{"Cmd":["/bin/sh -c #(nop) ADD file:a3bc1e842b69636f9df5256c49c5374fb4eef1e281fe3f282c65fb853ee171c5 in /"]}}`,
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(manifest.History) != len(expectedV1Compatibility) {
|
|
||||||
t.Fatalf("wrong number of history entries: %d", len(manifest.History))
|
|
||||||
}
|
|
||||||
for i := range expectedV1Compatibility {
|
|
||||||
if manifest.History[i].V1Compatibility != expectedV1Compatibility[i] {
|
|
||||||
t.Errorf("wrong V1Compatibility %d. expected:\n%s\ngot:\n%s", i, expectedV1Compatibility[i], manifest.History[i].V1Compatibility)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,226 +0,0 @@
|
||||||
// Package schema1 provides definitions for the deprecated Docker Image
|
|
||||||
// Manifest v2, Schema 1 specification.
|
|
||||||
//
|
|
||||||
// Deprecated: Docker Image Manifest v2, Schema 1 is deprecated since 2015.
|
|
||||||
// Use Docker Image Manifest v2, Schema 2, or the OCI Image Specification. This package should not be used for purposes
|
|
||||||
// other than backward compatibility.
|
|
||||||
package schema1
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/distribution/distribution/v3"
|
|
||||||
"github.com/distribution/distribution/v3/manifest"
|
|
||||||
"github.com/docker/libtrust"
|
|
||||||
"github.com/opencontainers/go-digest"
|
|
||||||
)
|
|
||||||
|
|
||||||
// MediaTypeManifest specifies the mediaType for the current version. Note
|
|
||||||
// that for schema version 1, the the media is optionally "application/json".
|
|
||||||
//
|
|
||||||
// Deprecated: Docker Image Manifest v2, Schema 1 is deprecated since 2015.
|
|
||||||
// Use Docker Image Manifest v2, Schema 2, or the OCI Image Specification.
|
|
||||||
const MediaTypeManifest = "application/vnd.docker.distribution.manifest.v1+json"
|
|
||||||
|
|
||||||
// MediaTypeSignedManifest specifies the mediatype for current SignedManifest version
|
|
||||||
//
|
|
||||||
// Deprecated: Docker Image Manifest v2, Schema 1 is deprecated since 2015.
|
|
||||||
// Use Docker Image Manifest v2, Schema 2, or the OCI Image Specification.
|
|
||||||
const MediaTypeSignedManifest = "application/vnd.docker.distribution.manifest.v1+prettyjws"
|
|
||||||
|
|
||||||
// MediaTypeManifestLayer specifies the media type for manifest layers
|
|
||||||
//
|
|
||||||
// Deprecated: Docker Image Manifest v2, Schema 1 is deprecated since 2015.
|
|
||||||
// Use Docker Image Manifest v2, Schema 2, or the OCI Image Specification.
|
|
||||||
const MediaTypeManifestLayer = "application/vnd.docker.container.image.rootfs.diff+x-gtar"
|
|
||||||
|
|
||||||
// SchemaVersion provides a pre-initialized version structure for this
|
|
||||||
// packages version of the manifest.
|
|
||||||
//
|
|
||||||
// Deprecated: Docker Image Manifest v2, Schema 1 is deprecated since 2015.
|
|
||||||
// Use Docker Image Manifest v2, Schema 2, or the OCI Image Specification.
|
|
||||||
var SchemaVersion = manifest.Versioned{
|
|
||||||
SchemaVersion: 1,
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
schema1Func := func(b []byte) (distribution.Manifest, distribution.Descriptor, error) {
|
|
||||||
sm := new(SignedManifest)
|
|
||||||
err := sm.UnmarshalJSON(b)
|
|
||||||
if err != nil {
|
|
||||||
return nil, distribution.Descriptor{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
desc := distribution.Descriptor{
|
|
||||||
MediaType: MediaTypeSignedManifest,
|
|
||||||
Digest: digest.FromBytes(sm.Canonical),
|
|
||||||
Size: int64(len(sm.Canonical)),
|
|
||||||
}
|
|
||||||
return sm, desc, err
|
|
||||||
}
|
|
||||||
err := distribution.RegisterManifestSchema(MediaTypeSignedManifest, schema1Func)
|
|
||||||
if err != nil {
|
|
||||||
panic(fmt.Sprintf("Unable to register manifest: %s", err))
|
|
||||||
}
|
|
||||||
err = distribution.RegisterManifestSchema("", schema1Func)
|
|
||||||
if err != nil {
|
|
||||||
panic(fmt.Sprintf("Unable to register manifest: %s", err))
|
|
||||||
}
|
|
||||||
err = distribution.RegisterManifestSchema("application/json", schema1Func)
|
|
||||||
if err != nil {
|
|
||||||
panic(fmt.Sprintf("Unable to register manifest: %s", err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// FSLayer is a container struct for BlobSums defined in an image manifest.
|
|
||||||
//
|
|
||||||
// Deprecated: Docker Image Manifest v2, Schema 1 is deprecated since 2015.
|
|
||||||
// Use Docker Image Manifest v2, Schema 2, or the OCI Image Specification.
|
|
||||||
type FSLayer struct {
|
|
||||||
// BlobSum is the tarsum of the referenced filesystem image layer
|
|
||||||
BlobSum digest.Digest `json:"blobSum"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// History stores unstructured v1 compatibility information.
|
|
||||||
//
|
|
||||||
// Deprecated: Docker Image Manifest v2, Schema 1 is deprecated since 2015.
|
|
||||||
// Use Docker Image Manifest v2, Schema 2, or the OCI Image Specification.
|
|
||||||
type History struct {
|
|
||||||
// V1Compatibility is the raw v1 compatibility information
|
|
||||||
V1Compatibility string `json:"v1Compatibility"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Manifest provides the base accessible fields for working with V2 image
|
|
||||||
// format in the registry.
|
|
||||||
//
|
|
||||||
// Deprecated: Docker Image Manifest v2, Schema 1 is deprecated since 2015.
|
|
||||||
// Use Docker Image Manifest v2, Schema 2, or the OCI Image Specification.
|
|
||||||
type Manifest struct {
|
|
||||||
manifest.Versioned
|
|
||||||
|
|
||||||
// Name is the name of the image's repository
|
|
||||||
Name string `json:"name"`
|
|
||||||
|
|
||||||
// Tag is the tag of the image specified by this manifest
|
|
||||||
Tag string `json:"tag"`
|
|
||||||
|
|
||||||
// Architecture is the host architecture on which this image is intended to
|
|
||||||
// run
|
|
||||||
Architecture string `json:"architecture"`
|
|
||||||
|
|
||||||
// FSLayers is a list of filesystem layer blobSums contained in this image
|
|
||||||
FSLayers []FSLayer `json:"fsLayers"`
|
|
||||||
|
|
||||||
// History is a list of unstructured historical data for v1 compatibility
|
|
||||||
History []History `json:"history"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// SignedManifest provides an envelope for a signed image manifest, including
|
|
||||||
// the format sensitive raw bytes.
|
|
||||||
//
|
|
||||||
// Deprecated: Docker Image Manifest v2, Schema 1 is deprecated since 2015.
|
|
||||||
// Use Docker Image Manifest v2, Schema 2, or the OCI Image Specification.
|
|
||||||
type SignedManifest struct {
|
|
||||||
Manifest
|
|
||||||
|
|
||||||
// Canonical is the canonical byte representation of the ImageManifest,
|
|
||||||
// without any attached signatures. The manifest byte
|
|
||||||
// representation cannot change or it will have to be re-signed.
|
|
||||||
Canonical []byte `json:"-"`
|
|
||||||
|
|
||||||
// all contains the byte representation of the Manifest including signatures
|
|
||||||
// and is returned by Payload()
|
|
||||||
all []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalJSON populates a new SignedManifest struct from JSON data.
|
|
||||||
//
|
|
||||||
// Deprecated: Docker Image Manifest v2, Schema 1 is deprecated since 2015.
|
|
||||||
// Use Docker Image Manifest v2, Schema 2, or the OCI Image Specification.
|
|
||||||
func (sm *SignedManifest) UnmarshalJSON(b []byte) error {
|
|
||||||
sm.all = make([]byte, len(b))
|
|
||||||
// store manifest and signatures in all
|
|
||||||
copy(sm.all, b)
|
|
||||||
|
|
||||||
jsig, err := libtrust.ParsePrettySignature(b, "signatures")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resolve the payload in the manifest.
|
|
||||||
bytes, err := jsig.Payload()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// sm.Canonical stores the canonical manifest JSON
|
|
||||||
sm.Canonical = make([]byte, len(bytes))
|
|
||||||
copy(sm.Canonical, bytes)
|
|
||||||
|
|
||||||
// Unmarshal canonical JSON into Manifest object
|
|
||||||
var mfst Manifest
|
|
||||||
if err := json.Unmarshal(sm.Canonical, &mfst); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
sm.Manifest = mfst
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// References returns the descriptors of this manifests references.
|
|
||||||
//
|
|
||||||
// Deprecated: Docker Image Manifest v2, Schema 1 is deprecated since 2015.
|
|
||||||
// Use Docker Image Manifest v2, Schema 2, or the OCI Image Specification.
|
|
||||||
func (sm SignedManifest) References() []distribution.Descriptor {
|
|
||||||
dependencies := make([]distribution.Descriptor, len(sm.FSLayers))
|
|
||||||
for i, fsLayer := range sm.FSLayers {
|
|
||||||
dependencies[i] = distribution.Descriptor{
|
|
||||||
MediaType: "application/vnd.docker.container.image.rootfs.diff+x-gtar",
|
|
||||||
Digest: fsLayer.BlobSum,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return dependencies
|
|
||||||
}
|
|
||||||
|
|
||||||
// MarshalJSON returns the contents of raw. If Raw is nil, marshals the inner
|
|
||||||
// contents. Applications requiring a marshaled signed manifest should simply
|
|
||||||
// use Raw directly, since the the content produced by json.Marshal will be
|
|
||||||
// compacted and will fail signature checks.
|
|
||||||
//
|
|
||||||
// Deprecated: Docker Image Manifest v2, Schema 1 is deprecated since 2015.
|
|
||||||
// Use Docker Image Manifest v2, Schema 2, or the OCI Image Specification.
|
|
||||||
func (sm *SignedManifest) MarshalJSON() ([]byte, error) {
|
|
||||||
if len(sm.all) > 0 {
|
|
||||||
return sm.all, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the raw data is not available, just dump the inner content.
|
|
||||||
return json.Marshal(&sm.Manifest)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Payload returns the signed content of the signed manifest.
|
|
||||||
//
|
|
||||||
// Deprecated: Docker Image Manifest v2, Schema 1 is deprecated since 2015.
|
|
||||||
// Use Docker Image Manifest v2, Schema 2, or the OCI Image Specification.
|
|
||||||
func (sm SignedManifest) Payload() (string, []byte, error) {
|
|
||||||
return MediaTypeSignedManifest, sm.all, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Signatures returns the signatures as provided by
|
|
||||||
// (*libtrust.JSONSignature).Signatures. The byte slices are opaque jws
|
|
||||||
// signatures.
|
|
||||||
//
|
|
||||||
// Deprecated: Docker Image Manifest v2, Schema 1 is deprecated since 2015.
|
|
||||||
// Use Docker Image Manifest v2, Schema 2, or the OCI Image Specification.
|
|
||||||
func (sm *SignedManifest) Signatures() ([][]byte, error) {
|
|
||||||
jsig, err := libtrust.ParsePrettySignature(sm.all, "signatures")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resolve the payload in the manifest.
|
|
||||||
return jsig.Signatures()
|
|
||||||
}
|
|
|
@ -1,135 +0,0 @@
|
||||||
package schema1
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/docker/libtrust"
|
|
||||||
)
|
|
||||||
|
|
||||||
type testEnv struct {
|
|
||||||
name, tag string
|
|
||||||
invalidSigned *SignedManifest
|
|
||||||
signed *SignedManifest
|
|
||||||
pk libtrust.PrivateKey
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestManifestMarshaling(t *testing.T) {
|
|
||||||
env := genEnv(t)
|
|
||||||
|
|
||||||
// Check that the all field is the same as json.MarshalIndent with these
|
|
||||||
// parameters.
|
|
||||||
expected, err := json.MarshalIndent(env.signed, "", " ")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("error marshaling manifest: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !bytes.Equal(expected, env.signed.all) {
|
|
||||||
t.Fatalf("manifest bytes not equal:\nexpected:\n%s\nactual:\n%s\n", string(expected), string(env.signed.all))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestManifestUnmarshaling(t *testing.T) {
|
|
||||||
env := genEnv(t)
|
|
||||||
|
|
||||||
var signed SignedManifest
|
|
||||||
if err := json.Unmarshal(env.signed.all, &signed); err != nil {
|
|
||||||
t.Fatalf("error unmarshaling signed manifest: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !reflect.DeepEqual(&signed, env.signed) {
|
|
||||||
t.Fatalf("manifests are different after unmarshaling: %v != %v", signed, env.signed)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestManifestVerification(t *testing.T) {
|
|
||||||
env := genEnv(t)
|
|
||||||
|
|
||||||
publicKeys, err := Verify(env.signed)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("error verifying manifest: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(publicKeys) == 0 {
|
|
||||||
t.Fatalf("no public keys found in signature")
|
|
||||||
}
|
|
||||||
|
|
||||||
var found bool
|
|
||||||
publicKey := env.pk.PublicKey()
|
|
||||||
// ensure that one of the extracted public keys matches the private key.
|
|
||||||
for _, candidate := range publicKeys {
|
|
||||||
if candidate.KeyID() == publicKey.KeyID() {
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !found {
|
|
||||||
t.Fatalf("expected public key, %v, not found in verified keys: %v", publicKey, publicKeys)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check that an invalid manifest fails verification
|
|
||||||
_, err = Verify(env.invalidSigned)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Invalid manifest should not pass Verify()")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func genEnv(t *testing.T) *testEnv {
|
|
||||||
pk, err := libtrust.GenerateECP256PrivateKey()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("error generating test key: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
name, tag := "foo/bar", "test"
|
|
||||||
|
|
||||||
invalid := Manifest{
|
|
||||||
Versioned: SchemaVersion,
|
|
||||||
Name: name,
|
|
||||||
Tag: tag,
|
|
||||||
FSLayers: []FSLayer{
|
|
||||||
{
|
|
||||||
BlobSum: "asdf",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
BlobSum: "qwer",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
valid := Manifest{
|
|
||||||
Versioned: SchemaVersion,
|
|
||||||
Name: name,
|
|
||||||
Tag: tag,
|
|
||||||
FSLayers: []FSLayer{
|
|
||||||
{
|
|
||||||
BlobSum: "asdf",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
History: []History{
|
|
||||||
{
|
|
||||||
V1Compatibility: "",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
sm, err := Sign(&valid, pk)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("error signing manifest: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
invalidSigned, err := Sign(&invalid, pk)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("error signing manifest: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &testEnv{
|
|
||||||
name: name,
|
|
||||||
tag: tag,
|
|
||||||
invalidSigned: invalidSigned,
|
|
||||||
signed: sm,
|
|
||||||
pk: pk,
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,112 +0,0 @@
|
||||||
package schema1
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/distribution/distribution/v3"
|
|
||||||
"github.com/distribution/distribution/v3/manifest"
|
|
||||||
"github.com/distribution/distribution/v3/reference"
|
|
||||||
"github.com/docker/libtrust"
|
|
||||||
"github.com/opencontainers/go-digest"
|
|
||||||
)
|
|
||||||
|
|
||||||
// referenceManifestBuilder is a type for constructing manifests from schema1
|
|
||||||
// dependencies.
|
|
||||||
type referenceManifestBuilder struct {
|
|
||||||
Manifest
|
|
||||||
pk libtrust.PrivateKey
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewReferenceManifestBuilder is used to build new manifests for the current
|
|
||||||
// schema version using schema1 dependencies.
|
|
||||||
//
|
|
||||||
// Deprecated: Docker Image Manifest v2, Schema 1 is deprecated since 2015.
|
|
||||||
// Use Docker Image Manifest v2, Schema 2, or the OCI Image Specification.
|
|
||||||
func NewReferenceManifestBuilder(pk libtrust.PrivateKey, ref reference.Named, architecture string) distribution.ManifestBuilder {
|
|
||||||
tag := ""
|
|
||||||
if tagged, isTagged := ref.(reference.Tagged); isTagged {
|
|
||||||
tag = tagged.Tag()
|
|
||||||
}
|
|
||||||
|
|
||||||
return &referenceManifestBuilder{
|
|
||||||
Manifest: Manifest{
|
|
||||||
Versioned: manifest.Versioned{
|
|
||||||
SchemaVersion: 1,
|
|
||||||
},
|
|
||||||
Name: ref.Name(),
|
|
||||||
Tag: tag,
|
|
||||||
Architecture: architecture,
|
|
||||||
},
|
|
||||||
pk: pk,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mb *referenceManifestBuilder) Build(ctx context.Context) (distribution.Manifest, error) {
|
|
||||||
m := mb.Manifest
|
|
||||||
if len(m.FSLayers) == 0 {
|
|
||||||
return nil, errors.New("cannot build manifest with zero layers or history")
|
|
||||||
}
|
|
||||||
|
|
||||||
m.FSLayers = make([]FSLayer, len(mb.Manifest.FSLayers))
|
|
||||||
m.History = make([]History, len(mb.Manifest.History))
|
|
||||||
copy(m.FSLayers, mb.Manifest.FSLayers)
|
|
||||||
copy(m.History, mb.Manifest.History)
|
|
||||||
|
|
||||||
return Sign(&m, mb.pk)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AppendReference adds a reference to the current ManifestBuilder.
|
|
||||||
//
|
|
||||||
// Deprecated: Docker Image Manifest v2, Schema 1 is deprecated since 2015.
|
|
||||||
// Use Docker Image Manifest v2, Schema 2, or the OCI Image Specification.
|
|
||||||
func (mb *referenceManifestBuilder) AppendReference(d distribution.Describable) error {
|
|
||||||
r, ok := d.(Reference)
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("unable to add non-reference type to v1 builder")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Entries need to be prepended
|
|
||||||
mb.Manifest.FSLayers = append([]FSLayer{{BlobSum: r.Digest}}, mb.Manifest.FSLayers...)
|
|
||||||
mb.Manifest.History = append([]History{r.History}, mb.Manifest.History...)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// References returns the current references added to this builder.
|
|
||||||
//
|
|
||||||
// Deprecated: Docker Image Manifest v2, Schema 1 is deprecated since 2015.
|
|
||||||
// Use Docker Image Manifest v2, Schema 2, or the OCI Image Specification.
|
|
||||||
func (mb *referenceManifestBuilder) References() []distribution.Descriptor {
|
|
||||||
refs := make([]distribution.Descriptor, len(mb.Manifest.FSLayers))
|
|
||||||
for i := range mb.Manifest.FSLayers {
|
|
||||||
layerDigest := mb.Manifest.FSLayers[i].BlobSum
|
|
||||||
history := mb.Manifest.History[i]
|
|
||||||
ref := Reference{layerDigest, 0, history}
|
|
||||||
refs[i] = ref.Descriptor()
|
|
||||||
}
|
|
||||||
return refs
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reference describes a Manifest v2, schema version 1 dependency.
|
|
||||||
// An FSLayer associated with a history entry.
|
|
||||||
//
|
|
||||||
// Deprecated: Docker Image Manifest v2, Schema 1 is deprecated since 2015.
|
|
||||||
// Use Docker Image Manifest v2, Schema 2, or the OCI Image Specification.
|
|
||||||
type Reference struct {
|
|
||||||
Digest digest.Digest
|
|
||||||
Size int64 // if we know it, set it for the descriptor.
|
|
||||||
History History
|
|
||||||
}
|
|
||||||
|
|
||||||
// Descriptor describes a reference.
|
|
||||||
//
|
|
||||||
// Deprecated: Docker Image Manifest v2, Schema 1 is deprecated since 2015.
|
|
||||||
// Use Docker Image Manifest v2, Schema 2, or the OCI Image Specification.
|
|
||||||
func (r Reference) Descriptor() distribution.Descriptor {
|
|
||||||
return distribution.Descriptor{
|
|
||||||
MediaType: MediaTypeManifestLayer,
|
|
||||||
Digest: r.Digest,
|
|
||||||
Size: r.Size,
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,108 +0,0 @@
|
||||||
package schema1
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/distribution/distribution/v3/context"
|
|
||||||
"github.com/distribution/distribution/v3/manifest"
|
|
||||||
"github.com/distribution/distribution/v3/reference"
|
|
||||||
"github.com/docker/libtrust"
|
|
||||||
"github.com/opencontainers/go-digest"
|
|
||||||
)
|
|
||||||
|
|
||||||
func makeSignedManifest(t *testing.T, pk libtrust.PrivateKey, refs []Reference) *SignedManifest {
|
|
||||||
u := &Manifest{
|
|
||||||
Versioned: manifest.Versioned{
|
|
||||||
SchemaVersion: 1,
|
|
||||||
},
|
|
||||||
Name: "foo/bar",
|
|
||||||
Tag: "latest",
|
|
||||||
Architecture: "amd64",
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := len(refs) - 1; i >= 0; i-- {
|
|
||||||
u.FSLayers = append(u.FSLayers, FSLayer{
|
|
||||||
BlobSum: refs[i].Digest,
|
|
||||||
})
|
|
||||||
u.History = append(u.History, History{
|
|
||||||
V1Compatibility: refs[i].History.V1Compatibility,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
signedManifest, err := Sign(u, pk)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unexpected error signing manifest: %v", err)
|
|
||||||
}
|
|
||||||
return signedManifest
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestReferenceBuilder(t *testing.T) {
|
|
||||||
pk, err := libtrust.GenerateECP256PrivateKey()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unexpected error generating private key: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
r1 := Reference{
|
|
||||||
Digest: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
|
||||||
Size: 1,
|
|
||||||
History: History{V1Compatibility: "{\"a\" : 1 }"},
|
|
||||||
}
|
|
||||||
r2 := Reference{
|
|
||||||
Digest: "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
|
||||||
Size: 2,
|
|
||||||
History: History{V1Compatibility: "{\"\a\" : 2 }"},
|
|
||||||
}
|
|
||||||
|
|
||||||
handCrafted := makeSignedManifest(t, pk, []Reference{r1, r2})
|
|
||||||
|
|
||||||
ref, err := reference.WithName(handCrafted.Manifest.Name)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("could not parse reference: %v", err)
|
|
||||||
}
|
|
||||||
ref, err = reference.WithTag(ref, handCrafted.Manifest.Tag)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("could not add tag: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
b := NewReferenceManifestBuilder(pk, ref, handCrafted.Manifest.Architecture)
|
|
||||||
_, err = b.Build(context.Background())
|
|
||||||
if err == nil {
|
|
||||||
t.Fatal("Expected error building zero length manifest")
|
|
||||||
}
|
|
||||||
|
|
||||||
err = b.AppendReference(r1)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = b.AppendReference(r2)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
refs := b.References()
|
|
||||||
if len(refs) != 2 {
|
|
||||||
t.Fatalf("Unexpected reference count : %d != %d", 2, len(refs))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure ordering
|
|
||||||
if refs[0].Digest != r2.Digest {
|
|
||||||
t.Fatalf("Unexpected reference : %v", refs[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
m, err := b.Build(context.Background())
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
built, ok := m.(*SignedManifest)
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("unexpected type from Build() : %T", built)
|
|
||||||
}
|
|
||||||
|
|
||||||
d1 := digest.FromBytes(built.Canonical)
|
|
||||||
d2 := digest.FromBytes(handCrafted.Canonical)
|
|
||||||
if d1 != d2 {
|
|
||||||
t.Errorf("mismatching canonical JSON")
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,74 +0,0 @@
|
||||||
package schema1
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/x509"
|
|
||||||
"encoding/json"
|
|
||||||
|
|
||||||
"github.com/docker/libtrust"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Sign signs the manifest with the provided private key, returning a
|
|
||||||
// SignedManifest. This typically won't be used within the registry, except
|
|
||||||
// for testing.
|
|
||||||
//
|
|
||||||
// Deprecated: Docker Image Manifest v2, Schema 1 is deprecated since 2015.
|
|
||||||
// Use Docker Image Manifest v2, Schema 2, or the OCI Image Specification.
|
|
||||||
func Sign(m *Manifest, pk libtrust.PrivateKey) (*SignedManifest, error) {
|
|
||||||
p, err := json.MarshalIndent(m, "", " ")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
js, err := libtrust.NewJSONSignature(p)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := js.Sign(pk); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
pretty, err := js.PrettySignature("signatures")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &SignedManifest{
|
|
||||||
Manifest: *m,
|
|
||||||
all: pretty,
|
|
||||||
Canonical: p,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SignWithChain signs the manifest with the given private key and x509 chain.
|
|
||||||
// The public key of the first element in the chain must be the public key
|
|
||||||
// corresponding with the sign key.
|
|
||||||
//
|
|
||||||
// Deprecated: Docker Image Manifest v2, Schema 1 is deprecated since 2015.
|
|
||||||
// Use Docker Image Manifest v2, Schema 2, or the OCI Image Specification.
|
|
||||||
func SignWithChain(m *Manifest, key libtrust.PrivateKey, chain []*x509.Certificate) (*SignedManifest, error) {
|
|
||||||
p, err := json.MarshalIndent(m, "", " ")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
js, err := libtrust.NewJSONSignature(p)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := js.SignWithChain(key, chain); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
pretty, err := js.PrettySignature("signatures")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &SignedManifest{
|
|
||||||
Manifest: *m,
|
|
||||||
all: pretty,
|
|
||||||
Canonical: p,
|
|
||||||
}, nil
|
|
||||||
}
|
|
|
@ -1,38 +0,0 @@
|
||||||
package schema1
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/x509"
|
|
||||||
|
|
||||||
"github.com/docker/libtrust"
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Verify verifies the signature of the signed manifest returning the public
|
|
||||||
// keys used during signing.
|
|
||||||
//
|
|
||||||
// Deprecated: Docker Image Manifest v2, Schema 1 is deprecated since 2015.
|
|
||||||
// Use Docker Image Manifest v2, Schema 2, or the OCI Image Specification.
|
|
||||||
func Verify(sm *SignedManifest) ([]libtrust.PublicKey, error) {
|
|
||||||
js, err := libtrust.ParsePrettySignature(sm.all, "signatures")
|
|
||||||
if err != nil {
|
|
||||||
logrus.WithField("err", err).Debugf("(*SignedManifest).Verify")
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return js.Verify()
|
|
||||||
}
|
|
||||||
|
|
||||||
// VerifyChains verifies the signature of the signed manifest against the
|
|
||||||
// certificate pool returning the list of verified chains. Signatures without
|
|
||||||
// an x509 chain are not checked.
|
|
||||||
//
|
|
||||||
// Deprecated: Docker Image Manifest v2, Schema 1 is deprecated since 2015.
|
|
||||||
// Use Docker Image Manifest v2, Schema 2, or the OCI Image Specification.
|
|
||||||
func VerifyChains(sm *SignedManifest, ca *x509.CertPool) ([][]*x509.Certificate, error) {
|
|
||||||
js, err := libtrust.ParsePrettySignature(sm.all, "signatures")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return js.VerifyChains(ca)
|
|
||||||
}
|
|
|
@ -20,7 +20,7 @@ const (
|
||||||
// EventsMediaType is the mediatype for the json event envelope. If the
|
// EventsMediaType is the mediatype for the json event envelope. If the
|
||||||
// Event, ActorRecord, SourceRecord or Envelope structs change, the version
|
// Event, ActorRecord, SourceRecord or Envelope structs change, the version
|
||||||
// number should be incremented.
|
// number should be incremented.
|
||||||
EventsMediaType = "application/vnd.docker.distribution.events.v1+json"
|
EventsMediaType = "application/vnd.docker.distribution.events.v2+json"
|
||||||
// LayerMediaType is the media type for image rootfs diffs (aka "layers")
|
// LayerMediaType is the media type for image rootfs diffs (aka "layers")
|
||||||
// used by Docker. We don't expect this to change for quite a while.
|
// used by Docker. We don't expect this to change for quite a while.
|
||||||
layerMediaType = "application/vnd.docker.container.image.rootfs.diff+x-gtar"
|
layerMediaType = "application/vnd.docker.container.image.rootfs.diff+x-gtar"
|
||||||
|
|
Loading…
Reference in a new issue