Properly verify manifests and layer digests on pull
To ensure manifest integrity when pulling by digest, this changeset ensures that not only the remote digest provided by the registry is verified but also that the digest provided on the command line is checked, as well. If this check fails, the pull is cancelled as with an error. Inspection also should that while layers were being verified against their digests, the error was being treated as tech preview image signing verification error. This, in fact, is not a tech preview and opens up the docker daemon to man in the middle attacks that can be avoided with the v2 registry protocol. As a matter of cleanliness, the digest package from the distribution project has been updated to latest version. There were some recent improvements in the digest package. Signed-off-by: Stephen J Day <stephen.day@docker.com>
This commit is contained in:
parent
d79654d0f6
commit
06612cc0fe
19 changed files with 822 additions and 180 deletions
|
@ -8,62 +8,106 @@ import (
|
|||
"github.com/docker/distribution/digest"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/docker/docker/trust"
|
||||
"github.com/docker/docker/utils"
|
||||
"github.com/docker/libtrust"
|
||||
)
|
||||
|
||||
// loadManifest loads a manifest from a byte array and verifies its content.
|
||||
// The signature must be verified or an error is returned. If the manifest
|
||||
// contains no signatures by a trusted key for the name in the manifest, the
|
||||
// image is not considered verified. The parsed manifest object and a boolean
|
||||
// for whether the manifest is verified is returned.
|
||||
func (s *TagStore) loadManifest(manifestBytes []byte, dgst, ref string) (*registry.ManifestData, bool, error) {
|
||||
// loadManifest loads a manifest from a byte array and verifies its content,
|
||||
// returning the local digest, the manifest itself, whether or not it was
|
||||
// verified. If ref is a digest, rather than a tag, this will be treated as
|
||||
// the local digest. An error will be returned if the signature verification
|
||||
// fails, local digest verification fails and, if provided, the remote digest
|
||||
// verification fails. The boolean return will only be false without error on
|
||||
// the failure of signatures trust check.
|
||||
func (s *TagStore) loadManifest(manifestBytes []byte, ref string, remoteDigest digest.Digest) (digest.Digest, *registry.ManifestData, bool, error) {
|
||||
sig, err := libtrust.ParsePrettySignature(manifestBytes, "signatures")
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("error parsing payload: %s", err)
|
||||
return "", nil, false, fmt.Errorf("error parsing payload: %s", err)
|
||||
}
|
||||
|
||||
keys, err := sig.Verify()
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("error verifying payload: %s", err)
|
||||
return "", nil, false, fmt.Errorf("error verifying payload: %s", err)
|
||||
}
|
||||
|
||||
payload, err := sig.Payload()
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("error retrieving payload: %s", err)
|
||||
return "", nil, false, fmt.Errorf("error retrieving payload: %s", err)
|
||||
}
|
||||
|
||||
var manifestDigest digest.Digest
|
||||
// TODO(stevvooe): It would be a lot better here to build up a stack of
|
||||
// verifiers, then push the bytes one time for signatures and digests, but
|
||||
// the manifests are typically small, so this optimization is not worth
|
||||
// hacking this code without further refactoring.
|
||||
|
||||
if dgst != "" {
|
||||
manifestDigest, err = digest.ParseDigest(dgst)
|
||||
var localDigest digest.Digest
|
||||
|
||||
// verify the local digest, if present in tag
|
||||
if dgst, err := digest.ParseDigest(ref); err != nil {
|
||||
logrus.Debugf("provided manifest reference %q is not a digest: %v", ref, err)
|
||||
|
||||
// we don't have a local digest, since we are working from a tag.
|
||||
// Digest the payload and return that.
|
||||
localDigest, err = digest.FromBytes(payload)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("invalid manifest digest from registry: %s", err)
|
||||
// near impossible
|
||||
logrus.Errorf("error calculating local digest during tag pull: %v", err)
|
||||
return "", nil, false, err
|
||||
}
|
||||
|
||||
dgstVerifier, err := digest.NewDigestVerifier(manifestDigest)
|
||||
} else {
|
||||
// verify the manifest against local ref
|
||||
verifier, err := digest.NewDigestVerifier(dgst)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("unable to verify manifest digest from registry: %s", err)
|
||||
// There are not many ways this can go wrong: if it does, its
|
||||
// fatal. Likley, the cause would be poor validation of the
|
||||
// incoming reference.
|
||||
return "", nil, false, fmt.Errorf("error creating verifier for local digest reference %q: %v", dgst, err)
|
||||
}
|
||||
|
||||
dgstVerifier.Write(payload)
|
||||
|
||||
if !dgstVerifier.Verified() {
|
||||
computedDigest, _ := digest.FromBytes(payload)
|
||||
return nil, false, fmt.Errorf("unable to verify manifest digest: registry has %q, computed %q", manifestDigest, computedDigest)
|
||||
if _, err := verifier.Write(payload); err != nil {
|
||||
return "", nil, false, fmt.Errorf("error writing payload to local digest reference verifier: %v", err)
|
||||
}
|
||||
|
||||
if !verifier.Verified() {
|
||||
return "", nil, false, fmt.Errorf("verification against local digest reference %q failed", dgst)
|
||||
}
|
||||
|
||||
localDigest = dgst
|
||||
}
|
||||
|
||||
if utils.DigestReference(ref) && ref != manifestDigest.String() {
|
||||
return nil, false, fmt.Errorf("mismatching image manifest digest: got %q, expected %q", manifestDigest, ref)
|
||||
// verify against the remote digest, if available
|
||||
if remoteDigest != "" {
|
||||
if err := remoteDigest.Validate(); err != nil {
|
||||
return "", nil, false, fmt.Errorf("error validating remote digest %q: %v", remoteDigest, err)
|
||||
}
|
||||
|
||||
verifier, err := digest.NewDigestVerifier(remoteDigest)
|
||||
if err != nil {
|
||||
// There are not many ways this can go wrong: if it does, its
|
||||
// fatal. Likley, the cause would be poor validation of the
|
||||
// incoming reference.
|
||||
return "", nil, false, fmt.Errorf("error creating verifier for remote digest %q: %v", remoteDigest, err)
|
||||
}
|
||||
|
||||
if _, err := verifier.Write(payload); err != nil {
|
||||
return "", nil, false, fmt.Errorf("error writing payload to remote digest verifier (verifier target %q): %v", remoteDigest, err)
|
||||
}
|
||||
|
||||
if !verifier.Verified() {
|
||||
return "", nil, false, fmt.Errorf("verification against remote digest %q failed", remoteDigest)
|
||||
}
|
||||
}
|
||||
|
||||
var manifest registry.ManifestData
|
||||
if err := json.Unmarshal(payload, &manifest); err != nil {
|
||||
return nil, false, fmt.Errorf("error unmarshalling manifest: %s", err)
|
||||
return "", nil, false, fmt.Errorf("error unmarshalling manifest: %s", err)
|
||||
}
|
||||
if manifest.SchemaVersion != 1 {
|
||||
return nil, false, fmt.Errorf("unsupported schema version: %d", manifest.SchemaVersion)
|
||||
return "", nil, false, fmt.Errorf("unsupported schema version: %d", manifest.SchemaVersion)
|
||||
}
|
||||
|
||||
// validate the contents of the manifest
|
||||
if err := validateManifest(&manifest); err != nil {
|
||||
return "", nil, false, err
|
||||
}
|
||||
|
||||
var verified bool
|
||||
|
@ -74,14 +118,14 @@ func (s *TagStore) loadManifest(manifestBytes []byte, dgst, ref string) (*regist
|
|||
}
|
||||
b, err := key.MarshalJSON()
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("error marshalling public key: %s", err)
|
||||
return "", nil, false, fmt.Errorf("error marshalling public key: %s", err)
|
||||
}
|
||||
// Check key has read/write permission (0x03)
|
||||
v, err := s.trustService.CheckKey(namespace, b, 0x03)
|
||||
if err != nil {
|
||||
vErr, ok := err.(trust.NotVerifiedError)
|
||||
if !ok {
|
||||
return nil, false, fmt.Errorf("error running key check: %s", err)
|
||||
return "", nil, false, fmt.Errorf("error running key check: %s", err)
|
||||
}
|
||||
logrus.Debugf("Key check result: %v", vErr)
|
||||
}
|
||||
|
@ -90,10 +134,10 @@ func (s *TagStore) loadManifest(manifestBytes []byte, dgst, ref string) (*regist
|
|||
logrus.Debug("Key check result: verified")
|
||||
}
|
||||
}
|
||||
return &manifest, verified, nil
|
||||
return localDigest, &manifest, verified, nil
|
||||
}
|
||||
|
||||
func checkValidManifest(manifest *registry.ManifestData) error {
|
||||
func validateManifest(manifest *registry.ManifestData) error {
|
||||
if len(manifest.FSLayers) != len(manifest.History) {
|
||||
return fmt.Errorf("length of history not equal to number of layers")
|
||||
}
|
||||
|
|
|
@ -458,17 +458,6 @@ func WriteStatus(requestedTag string, out io.Writer, sf *streamformatter.StreamF
|
|||
}
|
||||
}
|
||||
|
||||
// downloadInfo is used to pass information from download to extractor
|
||||
type downloadInfo struct {
|
||||
imgJSON []byte
|
||||
img *image.Image
|
||||
digest digest.Digest
|
||||
tmpFile *os.File
|
||||
length int64
|
||||
downloaded bool
|
||||
err chan error
|
||||
}
|
||||
|
||||
func (s *TagStore) pullV2Repository(r *registry.Session, out io.Writer, repoInfo *registry.RepositoryInfo, tag string, sf *streamformatter.StreamFormatter) error {
|
||||
endpoint, err := r.V2RegistryEndpoint(repoInfo.Index)
|
||||
if err != nil {
|
||||
|
@ -518,27 +507,34 @@ func (s *TagStore) pullV2Repository(r *registry.Session, out io.Writer, repoInfo
|
|||
func (s *TagStore) pullV2Tag(r *registry.Session, out io.Writer, endpoint *registry.Endpoint, repoInfo *registry.RepositoryInfo, tag string, sf *streamformatter.StreamFormatter, auth *registry.RequestAuthorization) (bool, error) {
|
||||
logrus.Debugf("Pulling tag from V2 registry: %q", tag)
|
||||
|
||||
manifestBytes, manifestDigest, err := r.GetV2ImageManifest(endpoint, repoInfo.RemoteName, tag, auth)
|
||||
remoteDigest, manifestBytes, err := r.GetV2ImageManifest(endpoint, repoInfo.RemoteName, tag, auth)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// loadManifest ensures that the manifest payload has the expected digest
|
||||
// if the tag is a digest reference.
|
||||
manifest, verified, err := s.loadManifest(manifestBytes, manifestDigest, tag)
|
||||
localDigest, manifest, verified, err := s.loadManifest(manifestBytes, tag, remoteDigest)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("error verifying manifest: %s", err)
|
||||
}
|
||||
|
||||
if err := checkValidManifest(manifest); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if verified {
|
||||
logrus.Printf("Image manifest for %s has been verified", utils.ImageReference(repoInfo.CanonicalName, tag))
|
||||
}
|
||||
out.Write(sf.FormatStatus(tag, "Pulling from %s", repoInfo.CanonicalName))
|
||||
|
||||
// downloadInfo is used to pass information from download to extractor
|
||||
type downloadInfo struct {
|
||||
imgJSON []byte
|
||||
img *image.Image
|
||||
digest digest.Digest
|
||||
tmpFile *os.File
|
||||
length int64
|
||||
downloaded bool
|
||||
err chan error
|
||||
}
|
||||
|
||||
downloads := make([]downloadInfo, len(manifest.FSLayers))
|
||||
|
||||
for i := len(manifest.FSLayers) - 1; i >= 0; i-- {
|
||||
|
@ -611,8 +607,7 @@ func (s *TagStore) pullV2Tag(r *registry.Session, out io.Writer, endpoint *regis
|
|||
out.Write(sf.FormatProgress(stringid.TruncateID(img.ID), "Verifying Checksum", nil))
|
||||
|
||||
if !verifier.Verified() {
|
||||
logrus.Infof("Image verification failed: checksum mismatch for %q", di.digest.String())
|
||||
verified = false
|
||||
return fmt.Errorf("image layer digest verification failed for %q", di.digest)
|
||||
}
|
||||
|
||||
out.Write(sf.FormatProgress(stringid.TruncateID(img.ID), "Download complete", nil))
|
||||
|
@ -689,15 +684,27 @@ func (s *TagStore) pullV2Tag(r *registry.Session, out io.Writer, endpoint *regis
|
|||
out.Write(sf.FormatStatus(utils.ImageReference(repoInfo.CanonicalName, tag), "The image you are pulling has been verified. Important: image verification is a tech preview feature and should not be relied on to provide security."))
|
||||
}
|
||||
|
||||
if manifestDigest != "" {
|
||||
out.Write(sf.FormatStatus("", "Digest: %s", manifestDigest))
|
||||
if localDigest != remoteDigest { // this is not a verification check.
|
||||
// NOTE(stevvooe): This is a very defensive branch and should never
|
||||
// happen, since all manifest digest implementations use the same
|
||||
// algorithm.
|
||||
logrus.WithFields(
|
||||
logrus.Fields{
|
||||
"local": localDigest,
|
||||
"remote": remoteDigest,
|
||||
}).Debugf("local digest does not match remote")
|
||||
|
||||
out.Write(sf.FormatStatus("", "Remote Digest: %s", remoteDigest))
|
||||
}
|
||||
|
||||
if utils.DigestReference(tag) {
|
||||
if err = s.SetDigest(repoInfo.LocalName, tag, downloads[0].img.ID); err != nil {
|
||||
return false, err
|
||||
}
|
||||
} else {
|
||||
out.Write(sf.FormatStatus("", "Digest: %s", localDigest))
|
||||
|
||||
// always set the digest so we can use the digest whether we pull by it or not.
|
||||
if err = s.SetDigest(repoInfo.LocalName, localDigest.String(), downloads[0].img.ID); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if !utils.DigestReference(tag) {
|
||||
// only set the repository/tag -> image ID mapping when pulling by tag (i.e. not by digest)
|
||||
if err = s.Tag(repoInfo.LocalName, tag, downloads[0].img.ID, true); err != nil {
|
||||
return false, err
|
||||
|
|
|
@ -413,7 +413,7 @@ func (s *TagStore) pushV2Repository(r *registry.Session, localRepo Repository, o
|
|||
m.History[i] = ®istry.ManifestHistory{V1Compatibility: string(jsonData)}
|
||||
}
|
||||
|
||||
if err := checkValidManifest(m); err != nil {
|
||||
if err := validateManifest(m); err != nil {
|
||||
return fmt.Errorf("invalid manifest: %s", err)
|
||||
}
|
||||
|
||||
|
|
|
@ -60,7 +60,7 @@ clone git github.com/vishvananda/netns 008d17ae001344769b031375bdb38a86219154c6
|
|||
clone git github.com/vishvananda/netlink 8eb64238879fed52fd51c5b30ad20b928fb4c36c
|
||||
|
||||
# get distribution packages
|
||||
clone git github.com/docker/distribution d957768537c5af40e4f4cd96871f7b2bde9e2923
|
||||
clone git github.com/docker/distribution b9eeb328080d367dbde850ec6e94f1e4ac2b5efe
|
||||
mv src/github.com/docker/distribution/digest tmp-digest
|
||||
mv src/github.com/docker/distribution/registry/api tmp-api
|
||||
rm -rf src/github.com/docker/distribution
|
||||
|
|
|
@ -68,10 +68,15 @@ func (r *Session) GetV2Authorization(ep *Endpoint, imageName string, readOnly bo
|
|||
// 1.c) if anything else, err
|
||||
// 2) PUT the created/signed manifest
|
||||
//
|
||||
func (r *Session) GetV2ImageManifest(ep *Endpoint, imageName, tagName string, auth *RequestAuthorization) ([]byte, string, error) {
|
||||
|
||||
// GetV2ImageManifest simply fetches the bytes of a manifest and the remote
|
||||
// digest, if available in the request. Note that the application shouldn't
|
||||
// rely on the untrusted remoteDigest, and should also verify against a
|
||||
// locally provided digest, if applicable.
|
||||
func (r *Session) GetV2ImageManifest(ep *Endpoint, imageName, tagName string, auth *RequestAuthorization) (remoteDigest digest.Digest, p []byte, err error) {
|
||||
routeURL, err := getV2Builder(ep).BuildManifestURL(imageName, tagName)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
method := "GET"
|
||||
|
@ -79,31 +84,45 @@ func (r *Session) GetV2ImageManifest(ep *Endpoint, imageName, tagName string, au
|
|||
|
||||
req, err := http.NewRequest(method, routeURL, nil)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
if err := auth.Authorize(req); err != nil {
|
||||
return nil, "", err
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
res, err := r.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
return "", nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
if res.StatusCode != 200 {
|
||||
if res.StatusCode == 401 {
|
||||
return nil, "", errLoginRequired
|
||||
return "", nil, errLoginRequired
|
||||
} else if res.StatusCode == 404 {
|
||||
return nil, "", ErrDoesNotExist
|
||||
return "", nil, ErrDoesNotExist
|
||||
}
|
||||
return nil, "", httputils.NewHTTPRequestError(fmt.Sprintf("Server error: %d trying to fetch for %s:%s", res.StatusCode, imageName, tagName), res)
|
||||
return "", nil, httputils.NewHTTPRequestError(fmt.Sprintf("Server error: %d trying to fetch for %s:%s", res.StatusCode, imageName, tagName), res)
|
||||
}
|
||||
|
||||
manifestBytes, err := ioutil.ReadAll(res.Body)
|
||||
p, err = ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("Error while reading the http response: %s", err)
|
||||
return "", nil, fmt.Errorf("Error while reading the http response: %s", err)
|
||||
}
|
||||
|
||||
return manifestBytes, res.Header.Get(DockerDigestHeader), nil
|
||||
dgstHdr := res.Header.Get(DockerDigestHeader)
|
||||
if dgstHdr != "" {
|
||||
remoteDigest, err = digest.ParseDigest(dgstHdr)
|
||||
if err != nil {
|
||||
// NOTE(stevvooe): Including the remote digest is optional. We
|
||||
// don't need to verify against it, but it is good practice.
|
||||
remoteDigest = ""
|
||||
logrus.Debugf("error parsing remote digest when fetching %v: %v", routeURL, err)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// - Succeeded to head image blob (already exists)
|
||||
|
|
|
@ -2,7 +2,6 @@ package digest
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"hash"
|
||||
"io"
|
||||
|
@ -16,6 +15,7 @@ import (
|
|||
const (
|
||||
// DigestTarSumV1EmptyTar is the digest for the empty tar file.
|
||||
DigestTarSumV1EmptyTar = "tarsum.v1+sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
|
||||
// DigestSha256EmptyTar is the canonical sha256 digest of empty data
|
||||
DigestSha256EmptyTar = "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
)
|
||||
|
@ -39,7 +39,7 @@ const (
|
|||
type Digest string
|
||||
|
||||
// NewDigest returns a Digest from alg and a hash.Hash object.
|
||||
func NewDigest(alg string, h hash.Hash) Digest {
|
||||
func NewDigest(alg Algorithm, h hash.Hash) Digest {
|
||||
return Digest(fmt.Sprintf("%s:%x", alg, h.Sum(nil)))
|
||||
}
|
||||
|
||||
|
@ -72,13 +72,13 @@ func ParseDigest(s string) (Digest, error) {
|
|||
|
||||
// FromReader returns the most valid digest for the underlying content.
|
||||
func FromReader(rd io.Reader) (Digest, error) {
|
||||
h := sha256.New()
|
||||
digester := Canonical.New()
|
||||
|
||||
if _, err := io.Copy(h, rd); err != nil {
|
||||
if _, err := io.Copy(digester.Hash(), rd); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return NewDigest("sha256", h), nil
|
||||
return digester.Digest(), nil
|
||||
}
|
||||
|
||||
// FromTarArchive produces a tarsum digest from reader rd.
|
||||
|
@ -131,8 +131,8 @@ func (d Digest) Validate() error {
|
|||
return ErrDigestInvalidFormat
|
||||
}
|
||||
|
||||
switch s[:i] {
|
||||
case "sha256", "sha384", "sha512":
|
||||
switch Algorithm(s[:i]) {
|
||||
case SHA256, SHA384, SHA512:
|
||||
break
|
||||
default:
|
||||
return ErrDigestUnsupported
|
||||
|
@ -143,8 +143,8 @@ func (d Digest) Validate() error {
|
|||
|
||||
// Algorithm returns the algorithm portion of the digest. This will panic if
|
||||
// the underlying digest is not in a valid format.
|
||||
func (d Digest) Algorithm() string {
|
||||
return string(d[:d.sepIndex()])
|
||||
func (d Digest) Algorithm() Algorithm {
|
||||
return Algorithm(d[:d.sepIndex()])
|
||||
}
|
||||
|
||||
// Hex returns the hex digest portion of the digest. This will panic if the
|
||||
|
|
|
@ -10,7 +10,7 @@ func TestParseDigest(t *testing.T) {
|
|||
for _, testcase := range []struct {
|
||||
input string
|
||||
err error
|
||||
algorithm string
|
||||
algorithm Algorithm
|
||||
hex string
|
||||
}{
|
||||
{
|
||||
|
|
|
@ -1,44 +1,95 @@
|
|||
package digest
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"crypto"
|
||||
"hash"
|
||||
)
|
||||
|
||||
// Digester calculates the digest of written data. It is functionally
|
||||
// equivalent to hash.Hash but provides methods for returning the Digest type
|
||||
// rather than raw bytes.
|
||||
type Digester struct {
|
||||
alg string
|
||||
hash hash.Hash
|
||||
// Algorithm identifies and implementation of a digester by an identifier.
|
||||
// Note the that this defines both the hash algorithm used and the string
|
||||
// encoding.
|
||||
type Algorithm string
|
||||
|
||||
// supported digest types
|
||||
const (
|
||||
SHA256 Algorithm = "sha256" // sha256 with hex encoding
|
||||
SHA384 Algorithm = "sha384" // sha384 with hex encoding
|
||||
SHA512 Algorithm = "sha512" // sha512 with hex encoding
|
||||
TarsumV1SHA256 Algorithm = "tarsum+v1+sha256" // supported tarsum version, verification only
|
||||
|
||||
// Canonical is the primary digest algorithm used with the distribution
|
||||
// project. Other digests may be used but this one is the primary storage
|
||||
// digest.
|
||||
Canonical = SHA256
|
||||
)
|
||||
|
||||
var (
|
||||
// TODO(stevvooe): Follow the pattern of the standard crypto package for
|
||||
// registration of digests. Effectively, we are a registerable set and
|
||||
// common symbol access.
|
||||
|
||||
// algorithms maps values to hash.Hash implementations. Other algorithms
|
||||
// may be available but they cannot be calculated by the digest package.
|
||||
algorithms = map[Algorithm]crypto.Hash{
|
||||
SHA256: crypto.SHA256,
|
||||
SHA384: crypto.SHA384,
|
||||
SHA512: crypto.SHA512,
|
||||
}
|
||||
)
|
||||
|
||||
// Available returns true if the digest type is available for use. If this
|
||||
// returns false, New and Hash will return nil.
|
||||
func (a Algorithm) Available() bool {
|
||||
h, ok := algorithms[a]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
// check availability of the hash, as well
|
||||
return h.Available()
|
||||
}
|
||||
|
||||
// NewDigester create a new Digester with the given hashing algorithm and instance
|
||||
// of that algo's hasher.
|
||||
func NewDigester(alg string, h hash.Hash) Digester {
|
||||
return Digester{
|
||||
alg: alg,
|
||||
hash: h,
|
||||
// New returns a new digester for the specified algorithm. If the algorithm
|
||||
// does not have a digester implementation, nil will be returned. This can be
|
||||
// checked by calling Available before calling New.
|
||||
func (a Algorithm) New() Digester {
|
||||
return &digester{
|
||||
alg: a,
|
||||
hash: a.Hash(),
|
||||
}
|
||||
}
|
||||
|
||||
// NewCanonicalDigester is a convenience function to create a new Digester with
|
||||
// out default settings.
|
||||
func NewCanonicalDigester() Digester {
|
||||
return NewDigester("sha256", sha256.New())
|
||||
// Hash returns a new hash as used by the algorithm. If not available, nil is
|
||||
// returned. Make sure to check Available before calling.
|
||||
func (a Algorithm) Hash() hash.Hash {
|
||||
if !a.Available() {
|
||||
return nil
|
||||
}
|
||||
|
||||
return algorithms[a].New()
|
||||
}
|
||||
|
||||
// Write data to the digester. These writes cannot fail.
|
||||
func (d *Digester) Write(p []byte) (n int, err error) {
|
||||
return d.hash.Write(p)
|
||||
// TODO(stevvooe): Allow resolution of verifiers using the digest type and
|
||||
// this registration system.
|
||||
|
||||
// Digester calculates the digest of written data. Writes should go directly
|
||||
// to the return value of Hash, while calling Digest will return the current
|
||||
// value of the digest.
|
||||
type Digester interface {
|
||||
Hash() hash.Hash // provides direct access to underlying hash instance.
|
||||
Digest() Digest
|
||||
}
|
||||
|
||||
// Digest returns the current digest for this digester.
|
||||
func (d *Digester) Digest() Digest {
|
||||
// digester provides a simple digester definition that embeds a hasher.
|
||||
type digester struct {
|
||||
alg Algorithm
|
||||
hash hash.Hash
|
||||
}
|
||||
|
||||
func (d *digester) Hash() hash.Hash {
|
||||
return d.hash
|
||||
}
|
||||
|
||||
func (d *digester) Digest() Digest {
|
||||
return NewDigest(d.alg, d.hash)
|
||||
}
|
||||
|
||||
// Reset the state of the digester.
|
||||
func (d *Digester) Reset() {
|
||||
d.hash.Reset()
|
||||
}
|
||||
|
|
195
vendor/src/github.com/docker/distribution/digest/set.go
vendored
Normal file
195
vendor/src/github.com/docker/distribution/digest/set.go
vendored
Normal file
|
@ -0,0 +1,195 @@
|
|||
package digest
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrDigestNotFound is used when a matching digest
|
||||
// could not be found in a set.
|
||||
ErrDigestNotFound = errors.New("digest not found")
|
||||
|
||||
// ErrDigestAmbiguous is used when multiple digests
|
||||
// are found in a set. None of the matching digests
|
||||
// should be considered valid matches.
|
||||
ErrDigestAmbiguous = errors.New("ambiguous digest string")
|
||||
)
|
||||
|
||||
// Set is used to hold a unique set of digests which
|
||||
// may be easily referenced by easily referenced by a string
|
||||
// representation of the digest as well as short representation.
|
||||
// The uniqueness of the short representation is based on other
|
||||
// digests in the set. If digests are ommited from this set,
|
||||
// collisions in a larger set may not be detected, therefore it
|
||||
// is important to always do short representation lookups on
|
||||
// the complete set of digests. To mitigate collisions, an
|
||||
// appropriately long short code should be used.
|
||||
type Set struct {
|
||||
entries digestEntries
|
||||
}
|
||||
|
||||
// NewSet creates an empty set of digests
|
||||
// which may have digests added.
|
||||
func NewSet() *Set {
|
||||
return &Set{
|
||||
entries: digestEntries{},
|
||||
}
|
||||
}
|
||||
|
||||
// checkShortMatch checks whether two digests match as either whole
|
||||
// values or short values. This function does not test equality,
|
||||
// rather whether the second value could match against the first
|
||||
// value.
|
||||
func checkShortMatch(alg Algorithm, hex, shortAlg, shortHex string) bool {
|
||||
if len(hex) == len(shortHex) {
|
||||
if hex != shortHex {
|
||||
return false
|
||||
}
|
||||
if len(shortAlg) > 0 && string(alg) != shortAlg {
|
||||
return false
|
||||
}
|
||||
} else if !strings.HasPrefix(hex, shortHex) {
|
||||
return false
|
||||
} else if len(shortAlg) > 0 && string(alg) != shortAlg {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Lookup looks for a digest matching the given string representation.
|
||||
// If no digests could be found ErrDigestNotFound will be returned
|
||||
// with an empty digest value. If multiple matches are found
|
||||
// ErrDigestAmbiguous will be returned with an empty digest value.
|
||||
func (dst *Set) Lookup(d string) (Digest, error) {
|
||||
if len(dst.entries) == 0 {
|
||||
return "", ErrDigestNotFound
|
||||
}
|
||||
var (
|
||||
searchFunc func(int) bool
|
||||
alg Algorithm
|
||||
hex string
|
||||
)
|
||||
dgst, err := ParseDigest(d)
|
||||
if err == ErrDigestInvalidFormat {
|
||||
hex = d
|
||||
searchFunc = func(i int) bool {
|
||||
return dst.entries[i].val >= d
|
||||
}
|
||||
} else {
|
||||
hex = dgst.Hex()
|
||||
alg = dgst.Algorithm()
|
||||
searchFunc = func(i int) bool {
|
||||
if dst.entries[i].val == hex {
|
||||
return dst.entries[i].alg >= alg
|
||||
}
|
||||
return dst.entries[i].val >= hex
|
||||
}
|
||||
}
|
||||
idx := sort.Search(len(dst.entries), searchFunc)
|
||||
if idx == len(dst.entries) || !checkShortMatch(dst.entries[idx].alg, dst.entries[idx].val, string(alg), hex) {
|
||||
return "", ErrDigestNotFound
|
||||
}
|
||||
if dst.entries[idx].alg == alg && dst.entries[idx].val == hex {
|
||||
return dst.entries[idx].digest, nil
|
||||
}
|
||||
if idx+1 < len(dst.entries) && checkShortMatch(dst.entries[idx+1].alg, dst.entries[idx+1].val, string(alg), hex) {
|
||||
return "", ErrDigestAmbiguous
|
||||
}
|
||||
|
||||
return dst.entries[idx].digest, nil
|
||||
}
|
||||
|
||||
// Add adds the given digests to the set. An error will be returned
|
||||
// if the given digest is invalid. If the digest already exists in the
|
||||
// table, this operation will be a no-op.
|
||||
func (dst *Set) Add(d Digest) error {
|
||||
if err := d.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
entry := &digestEntry{alg: d.Algorithm(), val: d.Hex(), digest: d}
|
||||
searchFunc := func(i int) bool {
|
||||
if dst.entries[i].val == entry.val {
|
||||
return dst.entries[i].alg >= entry.alg
|
||||
}
|
||||
return dst.entries[i].val >= entry.val
|
||||
}
|
||||
idx := sort.Search(len(dst.entries), searchFunc)
|
||||
if idx == len(dst.entries) {
|
||||
dst.entries = append(dst.entries, entry)
|
||||
return nil
|
||||
} else if dst.entries[idx].digest == d {
|
||||
return nil
|
||||
}
|
||||
|
||||
entries := append(dst.entries, nil)
|
||||
copy(entries[idx+1:], entries[idx:len(entries)-1])
|
||||
entries[idx] = entry
|
||||
dst.entries = entries
|
||||
return nil
|
||||
}
|
||||
|
||||
// ShortCodeTable returns a map of Digest to unique short codes. The
|
||||
// length represents the minimum value, the maximum length may be the
|
||||
// entire value of digest if uniqueness cannot be achieved without the
|
||||
// full value. This function will attempt to make short codes as short
|
||||
// as possible to be unique.
|
||||
func ShortCodeTable(dst *Set, length int) map[Digest]string {
|
||||
m := make(map[Digest]string, len(dst.entries))
|
||||
l := length
|
||||
resetIdx := 0
|
||||
for i := 0; i < len(dst.entries); i++ {
|
||||
var short string
|
||||
extended := true
|
||||
for extended {
|
||||
extended = false
|
||||
if len(dst.entries[i].val) <= l {
|
||||
short = dst.entries[i].digest.String()
|
||||
} else {
|
||||
short = dst.entries[i].val[:l]
|
||||
for j := i + 1; j < len(dst.entries); j++ {
|
||||
if checkShortMatch(dst.entries[j].alg, dst.entries[j].val, "", short) {
|
||||
if j > resetIdx {
|
||||
resetIdx = j
|
||||
}
|
||||
extended = true
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
if extended {
|
||||
l++
|
||||
}
|
||||
}
|
||||
}
|
||||
m[dst.entries[i].digest] = short
|
||||
if i >= resetIdx {
|
||||
l = length
|
||||
}
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
type digestEntry struct {
|
||||
alg Algorithm
|
||||
val string
|
||||
digest Digest
|
||||
}
|
||||
|
||||
type digestEntries []*digestEntry
|
||||
|
||||
func (d digestEntries) Len() int {
|
||||
return len(d)
|
||||
}
|
||||
|
||||
func (d digestEntries) Less(i, j int) bool {
|
||||
if d[i].val != d[j].val {
|
||||
return d[i].val < d[j].val
|
||||
}
|
||||
return d[i].alg < d[j].alg
|
||||
}
|
||||
|
||||
func (d digestEntries) Swap(i, j int) {
|
||||
d[i], d[j] = d[j], d[i]
|
||||
}
|
272
vendor/src/github.com/docker/distribution/digest/set_test.go
vendored
Normal file
272
vendor/src/github.com/docker/distribution/digest/set_test.go
vendored
Normal file
|
@ -0,0 +1,272 @@
|
|||
package digest
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"math/rand"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func assertEqualDigests(t *testing.T, d1, d2 Digest) {
|
||||
if d1 != d2 {
|
||||
t.Fatalf("Digests do not match:\n\tActual: %s\n\tExpected: %s", d1, d2)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLookup(t *testing.T) {
|
||||
digests := []Digest{
|
||||
"sha256:12345",
|
||||
"sha256:1234",
|
||||
"sha256:12346",
|
||||
"sha256:54321",
|
||||
"sha256:65431",
|
||||
"sha256:64321",
|
||||
"sha256:65421",
|
||||
"sha256:65321",
|
||||
}
|
||||
|
||||
dset := NewSet()
|
||||
for i := range digests {
|
||||
if err := dset.Add(digests[i]); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
dgst, err := dset.Lookup("54")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assertEqualDigests(t, dgst, digests[3])
|
||||
|
||||
dgst, err = dset.Lookup("1234")
|
||||
if err == nil {
|
||||
t.Fatal("Expected ambiguous error looking up: 1234")
|
||||
}
|
||||
if err != ErrDigestAmbiguous {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
dgst, err = dset.Lookup("9876")
|
||||
if err == nil {
|
||||
t.Fatal("Expected ambiguous error looking up: 9876")
|
||||
}
|
||||
if err != ErrDigestNotFound {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
dgst, err = dset.Lookup("sha256:1234")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assertEqualDigests(t, dgst, digests[1])
|
||||
|
||||
dgst, err = dset.Lookup("sha256:12345")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assertEqualDigests(t, dgst, digests[0])
|
||||
|
||||
dgst, err = dset.Lookup("sha256:12346")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assertEqualDigests(t, dgst, digests[2])
|
||||
|
||||
dgst, err = dset.Lookup("12346")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assertEqualDigests(t, dgst, digests[2])
|
||||
|
||||
dgst, err = dset.Lookup("12345")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assertEqualDigests(t, dgst, digests[0])
|
||||
}
|
||||
|
||||
func TestAddDuplication(t *testing.T) {
|
||||
digests := []Digest{
|
||||
"sha256:1234",
|
||||
"sha256:12345",
|
||||
"sha256:12346",
|
||||
"sha256:54321",
|
||||
"sha256:65431",
|
||||
"sha512:65431",
|
||||
"sha512:65421",
|
||||
"sha512:65321",
|
||||
}
|
||||
|
||||
dset := NewSet()
|
||||
for i := range digests {
|
||||
if err := dset.Add(digests[i]); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(dset.entries) != 8 {
|
||||
t.Fatal("Invalid dset size")
|
||||
}
|
||||
|
||||
if err := dset.Add(Digest("sha256:12345")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(dset.entries) != 8 {
|
||||
t.Fatal("Duplicate digest insert allowed")
|
||||
}
|
||||
|
||||
if err := dset.Add(Digest("sha384:12345")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(dset.entries) != 9 {
|
||||
t.Fatal("Insert with different algorithm not allowed")
|
||||
}
|
||||
}
|
||||
|
||||
func assertEqualShort(t *testing.T, actual, expected string) {
|
||||
if actual != expected {
|
||||
t.Fatalf("Unexpected short value:\n\tExpected: %s\n\tActual: %s", expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestShortCodeTable(t *testing.T) {
|
||||
digests := []Digest{
|
||||
"sha256:1234",
|
||||
"sha256:12345",
|
||||
"sha256:12346",
|
||||
"sha256:54321",
|
||||
"sha256:65431",
|
||||
"sha256:64321",
|
||||
"sha256:65421",
|
||||
"sha256:65321",
|
||||
}
|
||||
|
||||
dset := NewSet()
|
||||
for i := range digests {
|
||||
if err := dset.Add(digests[i]); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
dump := ShortCodeTable(dset, 2)
|
||||
|
||||
if len(dump) < len(digests) {
|
||||
t.Fatalf("Error unexpected size: %d, expecting %d", len(dump), len(digests))
|
||||
}
|
||||
|
||||
assertEqualShort(t, dump[digests[0]], "sha256:1234")
|
||||
assertEqualShort(t, dump[digests[1]], "sha256:12345")
|
||||
assertEqualShort(t, dump[digests[2]], "sha256:12346")
|
||||
assertEqualShort(t, dump[digests[3]], "54")
|
||||
assertEqualShort(t, dump[digests[4]], "6543")
|
||||
assertEqualShort(t, dump[digests[5]], "64")
|
||||
assertEqualShort(t, dump[digests[6]], "6542")
|
||||
assertEqualShort(t, dump[digests[7]], "653")
|
||||
}
|
||||
|
||||
func createDigests(count int) ([]Digest, error) {
|
||||
r := rand.New(rand.NewSource(25823))
|
||||
digests := make([]Digest, count)
|
||||
for i := range digests {
|
||||
h := sha256.New()
|
||||
if err := binary.Write(h, binary.BigEndian, r.Int63()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
digests[i] = NewDigest("sha256", h)
|
||||
}
|
||||
return digests, nil
|
||||
}
|
||||
|
||||
func benchAddNTable(b *testing.B, n int) {
|
||||
digests, err := createDigests(n)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
dset := &Set{entries: digestEntries(make([]*digestEntry, 0, n))}
|
||||
for j := range digests {
|
||||
if err = dset.Add(digests[j]); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func benchLookupNTable(b *testing.B, n int, shortLen int) {
|
||||
digests, err := createDigests(n)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
dset := &Set{entries: digestEntries(make([]*digestEntry, 0, n))}
|
||||
for i := range digests {
|
||||
if err := dset.Add(digests[i]); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
shorts := make([]string, 0, n)
|
||||
for _, short := range ShortCodeTable(dset, shortLen) {
|
||||
shorts = append(shorts, short)
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
if _, err = dset.Lookup(shorts[i%n]); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func benchShortCodeNTable(b *testing.B, n int, shortLen int) {
|
||||
digests, err := createDigests(n)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
dset := &Set{entries: digestEntries(make([]*digestEntry, 0, n))}
|
||||
for i := range digests {
|
||||
if err := dset.Add(digests[i]); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
ShortCodeTable(dset, shortLen)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkAdd10(b *testing.B) {
|
||||
benchAddNTable(b, 10)
|
||||
}
|
||||
|
||||
func BenchmarkAdd100(b *testing.B) {
|
||||
benchAddNTable(b, 100)
|
||||
}
|
||||
|
||||
func BenchmarkAdd1000(b *testing.B) {
|
||||
benchAddNTable(b, 1000)
|
||||
}
|
||||
|
||||
func BenchmarkLookup10(b *testing.B) {
|
||||
benchLookupNTable(b, 10, 12)
|
||||
}
|
||||
|
||||
func BenchmarkLookup100(b *testing.B) {
|
||||
benchLookupNTable(b, 100, 12)
|
||||
}
|
||||
|
||||
func BenchmarkLookup1000(b *testing.B) {
|
||||
benchLookupNTable(b, 1000, 12)
|
||||
}
|
||||
|
||||
func BenchmarkShortCode10(b *testing.B) {
|
||||
benchShortCodeNTable(b, 10, 12)
|
||||
}
|
||||
func BenchmarkShortCode100(b *testing.B) {
|
||||
benchShortCodeNTable(b, 100, 12)
|
||||
}
|
||||
func BenchmarkShortCode1000(b *testing.B) {
|
||||
benchShortCodeNTable(b, 1000, 12)
|
||||
}
|
|
@ -6,10 +6,10 @@ import (
|
|||
"regexp"
|
||||
)
|
||||
|
||||
// TarSumRegexp defines a reguler expression to match tarsum identifiers.
|
||||
// TarSumRegexp defines a regular expression to match tarsum identifiers.
|
||||
var TarsumRegexp = regexp.MustCompile("tarsum(?:.[a-z0-9]+)?\\+[a-zA-Z0-9]+:[A-Fa-f0-9]+")
|
||||
|
||||
// TarsumRegexpCapturing defines a reguler expression to match tarsum identifiers with
|
||||
// TarsumRegexpCapturing defines a regular expression to match tarsum identifiers with
|
||||
// capture groups corresponding to each component.
|
||||
var TarsumRegexpCapturing = regexp.MustCompile("(tarsum)(.([a-z0-9]+))?\\+([a-zA-Z0-9]+):([A-Fa-f0-9]+)")
|
||||
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
package digest
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"crypto/sha512"
|
||||
"hash"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
|
@ -33,7 +31,7 @@ func NewDigestVerifier(d Digest) (Verifier, error) {
|
|||
switch alg {
|
||||
case "sha256", "sha384", "sha512":
|
||||
return hashVerifier{
|
||||
hash: newHash(alg),
|
||||
hash: alg.Hash(),
|
||||
digest: d,
|
||||
}, nil
|
||||
default:
|
||||
|
@ -95,19 +93,6 @@ func (lv *lengthVerifier) Verified() bool {
|
|||
return lv.expected == lv.len
|
||||
}
|
||||
|
||||
func newHash(name string) hash.Hash {
|
||||
switch name {
|
||||
case "sha256":
|
||||
return sha256.New()
|
||||
case "sha384":
|
||||
return sha512.New384()
|
||||
case "sha512":
|
||||
return sha512.New()
|
||||
default:
|
||||
panic("unsupport algorithm: " + name)
|
||||
}
|
||||
}
|
||||
|
||||
type hashVerifier struct {
|
||||
digest Digest
|
||||
hash hash.Hash
|
||||
|
|
|
@ -80,7 +80,7 @@ func TestVerifierUnsupportedDigest(t *testing.T) {
|
|||
}
|
||||
|
||||
if err != ErrDigestUnsupported {
|
||||
t.Fatalf("incorrect error for unsupported digest: %v %p %p", err, ErrDigestUnsupported, err)
|
||||
t.Fatalf("incorrect error for unsupported digest: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -28,7 +28,7 @@ var (
|
|||
Name: "uuid",
|
||||
Type: "opaque",
|
||||
Required: true,
|
||||
Description: `A uuid identifying the upload. This field can accept almost anything.`,
|
||||
Description: "A uuid identifying the upload. This field can accept characters that match `[a-zA-Z0-9-_.=]+`.",
|
||||
}
|
||||
|
||||
digestPathParameter = ParameterDescriptor{
|
||||
|
@ -135,7 +135,7 @@ const (
|
|||
"tag": <tag>,
|
||||
"fsLayers": [
|
||||
{
|
||||
"blobSum": <tarsum>
|
||||
"blobSum": "<digest>"
|
||||
},
|
||||
...
|
||||
]
|
||||
|
@ -606,7 +606,7 @@ var routeDescriptors = []RouteDescriptor{
|
|||
"code": "BLOB_UNKNOWN",
|
||||
"message": "blob unknown to registry",
|
||||
"detail": {
|
||||
"digest": <tarsum>
|
||||
"digest": "<digest>"
|
||||
}
|
||||
},
|
||||
...
|
||||
|
@ -712,7 +712,7 @@ var routeDescriptors = []RouteDescriptor{
|
|||
Name: RouteNameBlob,
|
||||
Path: "/v2/{name:" + RepositoryNameRegexp.String() + "}/blobs/{digest:" + digest.DigestRegexp.String() + "}",
|
||||
Entity: "Blob",
|
||||
Description: "Fetch the blob identified by `name` and `digest`. Used to fetch layers by tarsum digest.",
|
||||
Description: "Fetch the blob identified by `name` and `digest`. Used to fetch layers by digest.",
|
||||
Methods: []MethodDescriptor{
|
||||
|
||||
{
|
||||
|
@ -898,7 +898,7 @@ var routeDescriptors = []RouteDescriptor{
|
|||
{
|
||||
Name: "digest",
|
||||
Type: "query",
|
||||
Format: "<tarsum>",
|
||||
Format: "<digest>",
|
||||
Regexp: digest.DigestRegexp,
|
||||
Description: `Digest of uploaded blob. If present, the upload will be completed, in a single request, with contents of the request body as the resulting blob.`,
|
||||
},
|
||||
|
@ -985,7 +985,7 @@ var routeDescriptors = []RouteDescriptor{
|
|||
|
||||
{
|
||||
Name: RouteNameBlobUploadChunk,
|
||||
Path: "/v2/{name:" + RepositoryNameRegexp.String() + "}/blobs/uploads/{uuid}",
|
||||
Path: "/v2/{name:" + RepositoryNameRegexp.String() + "}/blobs/uploads/{uuid:[a-zA-Z0-9-_.=]+}",
|
||||
Entity: "Blob Upload",
|
||||
Description: "Interact with blob uploads. Clients should never assemble URLs for this endpoint and should only take it through the `Location` header on related API requests. The `Location` header and its parameters should be preserved by clients, using the latest value returned via upload related API calls.",
|
||||
Methods: []MethodDescriptor{
|
||||
|
@ -1055,7 +1055,74 @@ var routeDescriptors = []RouteDescriptor{
|
|||
Description: "Upload a chunk of data for the specified upload.",
|
||||
Requests: []RequestDescriptor{
|
||||
{
|
||||
Description: "Upload a chunk of data to specified upload without completing the upload.",
|
||||
Name: "Stream upload",
|
||||
Description: "Upload a stream of data to upload without completing the upload.",
|
||||
PathParameters: []ParameterDescriptor{
|
||||
nameParameterDescriptor,
|
||||
uuidParameterDescriptor,
|
||||
},
|
||||
Headers: []ParameterDescriptor{
|
||||
hostHeader,
|
||||
authHeader,
|
||||
},
|
||||
Body: BodyDescriptor{
|
||||
ContentType: "application/octet-stream",
|
||||
Format: "<binary data>",
|
||||
},
|
||||
Successes: []ResponseDescriptor{
|
||||
{
|
||||
Name: "Data Accepted",
|
||||
Description: "The stream of data has been accepted and the current progress is available in the range header. The updated upload location is available in the `Location` header.",
|
||||
StatusCode: http.StatusNoContent,
|
||||
Headers: []ParameterDescriptor{
|
||||
{
|
||||
Name: "Location",
|
||||
Type: "url",
|
||||
Format: "/v2/<name>/blobs/uploads/<uuid>",
|
||||
Description: "The location of the upload. Clients should assume this changes after each request. Clients should use the contents verbatim to complete the upload, adding parameters where required.",
|
||||
},
|
||||
{
|
||||
Name: "Range",
|
||||
Type: "header",
|
||||
Format: "0-<offset>",
|
||||
Description: "Range indicating the current progress of the upload.",
|
||||
},
|
||||
contentLengthZeroHeader,
|
||||
dockerUploadUUIDHeader,
|
||||
},
|
||||
},
|
||||
},
|
||||
Failures: []ResponseDescriptor{
|
||||
{
|
||||
Description: "There was an error processing the upload and it must be restarted.",
|
||||
StatusCode: http.StatusBadRequest,
|
||||
ErrorCodes: []ErrorCode{
|
||||
ErrorCodeDigestInvalid,
|
||||
ErrorCodeNameInvalid,
|
||||
ErrorCodeBlobUploadInvalid,
|
||||
},
|
||||
Body: BodyDescriptor{
|
||||
ContentType: "application/json; charset=utf-8",
|
||||
Format: errorsBody,
|
||||
},
|
||||
},
|
||||
unauthorizedResponsePush,
|
||||
{
|
||||
Description: "The upload is unknown to the registry. The upload must be restarted.",
|
||||
StatusCode: http.StatusNotFound,
|
||||
ErrorCodes: []ErrorCode{
|
||||
ErrorCodeBlobUploadUnknown,
|
||||
},
|
||||
Body: BodyDescriptor{
|
||||
ContentType: "application/json; charset=utf-8",
|
||||
Format: errorsBody,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Chunked upload",
|
||||
Description: "Upload a chunk of data to specified upload without completing the upload. The data will be uploaded to the specified Content Range.",
|
||||
PathParameters: []ParameterDescriptor{
|
||||
nameParameterDescriptor,
|
||||
uuidParameterDescriptor,
|
||||
|
@ -1143,26 +1210,15 @@ var routeDescriptors = []RouteDescriptor{
|
|||
Description: "Complete the upload specified by `uuid`, optionally appending the body as the final chunk.",
|
||||
Requests: []RequestDescriptor{
|
||||
{
|
||||
// TODO(stevvooe): Break this down into three separate requests:
|
||||
// 1. Complete an upload where all data has already been sent.
|
||||
// 2. Complete an upload where the entire body is in the PUT.
|
||||
// 3. Complete an upload where the final, partial chunk is the body.
|
||||
|
||||
Description: "Complete the upload, providing the _final_ chunk of data, if necessary. This method may take a body with all the data. If the `Content-Range` header is specified, it may include the final chunk. A request without a body will just complete the upload with previously uploaded content.",
|
||||
Description: "Complete the upload, providing all the data in the body, if necessary. A request without a body will just complete the upload with previously uploaded content.",
|
||||
Headers: []ParameterDescriptor{
|
||||
hostHeader,
|
||||
authHeader,
|
||||
{
|
||||
Name: "Content-Range",
|
||||
Type: "header",
|
||||
Format: "<start of range>-<end of range, inclusive>",
|
||||
Description: "Range of bytes identifying the block of content represented by the body. Start must the end offset retrieved via status check plus one. Note that this is a non-standard use of the `Content-Range` header. May be omitted if no data is provided.",
|
||||
},
|
||||
{
|
||||
Name: "Content-Length",
|
||||
Type: "integer",
|
||||
Format: "<length of chunk>",
|
||||
Description: "Length of the chunk being uploaded, corresponding to the length of the request body. May be zero if no data is provided.",
|
||||
Format: "<length of data>",
|
||||
Description: "Length of the data being uploaded, corresponding to the length of the request body. May be zero if no data is provided.",
|
||||
},
|
||||
},
|
||||
PathParameters: []ParameterDescriptor{
|
||||
|
@ -1173,7 +1229,7 @@ var routeDescriptors = []RouteDescriptor{
|
|||
{
|
||||
Name: "digest",
|
||||
Type: "string",
|
||||
Format: "<tarsum>",
|
||||
Format: "<digest>",
|
||||
Regexp: digest.DigestRegexp,
|
||||
Required: true,
|
||||
Description: `Digest of uploaded blob.`,
|
||||
|
@ -1181,7 +1237,7 @@ var routeDescriptors = []RouteDescriptor{
|
|||
},
|
||||
Body: BodyDescriptor{
|
||||
ContentType: "application/octet-stream",
|
||||
Format: "<binary chunk>",
|
||||
Format: "<binary data>",
|
||||
},
|
||||
Successes: []ResponseDescriptor{
|
||||
{
|
||||
|
@ -1190,9 +1246,10 @@ var routeDescriptors = []RouteDescriptor{
|
|||
StatusCode: http.StatusNoContent,
|
||||
Headers: []ParameterDescriptor{
|
||||
{
|
||||
Name: "Location",
|
||||
Type: "url",
|
||||
Format: "<blob location>",
|
||||
Name: "Location",
|
||||
Type: "url",
|
||||
Format: "<blob location>",
|
||||
Description: "The canonical location of the blob for retrieval",
|
||||
},
|
||||
{
|
||||
Name: "Content-Range",
|
||||
|
@ -1200,12 +1257,7 @@ var routeDescriptors = []RouteDescriptor{
|
|||
Format: "<start of range>-<end of range, inclusive>",
|
||||
Description: "Range of bytes identifying the desired block of content represented by the body. Start must match the end of offset retrieved via status check. Note that this is a non-standard use of the `Content-Range` header.",
|
||||
},
|
||||
{
|
||||
Name: "Content-Length",
|
||||
Type: "integer",
|
||||
Format: "<length of chunk>",
|
||||
Description: "Length of the chunk being uploaded, corresponding the length of the request body.",
|
||||
},
|
||||
contentLengthZeroHeader,
|
||||
digestHeader,
|
||||
},
|
||||
},
|
||||
|
@ -1236,24 +1288,6 @@ var routeDescriptors = []RouteDescriptor{
|
|||
Format: errorsBody,
|
||||
},
|
||||
},
|
||||
{
|
||||
Description: "The `Content-Range` specification cannot be accepted, either because it does not overlap with the current progress or it is invalid. The contents of the `Range` header may be used to resolve the condition.",
|
||||
StatusCode: http.StatusRequestedRangeNotSatisfiable,
|
||||
Headers: []ParameterDescriptor{
|
||||
{
|
||||
Name: "Location",
|
||||
Type: "url",
|
||||
Format: "/v2/<name>/blobs/uploads/<uuid>",
|
||||
Description: "The location of the upload. Clients should assume this changes after each request. Clients should use the contents verbatim to complete the upload, adding parameters where required.",
|
||||
},
|
||||
{
|
||||
Name: "Range",
|
||||
Type: "header",
|
||||
Format: "0-<offset>",
|
||||
Description: "Range indicating the current progress of the upload.",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -46,7 +46,7 @@ var (
|
|||
// ErrRepositoryNameComponentShort is returned when a repository name
|
||||
// contains a component which is shorter than
|
||||
// RepositoryNameComponentMinLength
|
||||
ErrRepositoryNameComponentShort = fmt.Errorf("respository name component must be %v or more characters", RepositoryNameComponentMinLength)
|
||||
ErrRepositoryNameComponentShort = fmt.Errorf("repository name component must be %v or more characters", RepositoryNameComponentMinLength)
|
||||
|
||||
// ErrRepositoryNameMissingComponents is returned when a repository name
|
||||
// contains fewer than RepositoryNameMinComponents components
|
||||
|
@ -61,7 +61,7 @@ var (
|
|||
ErrRepositoryNameComponentInvalid = fmt.Errorf("repository name component must match %q", RepositoryNameComponentRegexp.String())
|
||||
)
|
||||
|
||||
// ValidateRespositoryName ensures the repository name is valid for use in the
|
||||
// ValidateRepositoryName ensures the repository name is valid for use in the
|
||||
// registry. This function accepts a superset of what might be accepted by
|
||||
// docker core or docker hub. If the name does not pass validation, an error,
|
||||
// describing the conditions, is returned.
|
||||
|
@ -75,7 +75,7 @@ var (
|
|||
//
|
||||
// The result of the production, known as the "namespace", should be limited
|
||||
// to 255 characters.
|
||||
func ValidateRespositoryName(name string) error {
|
||||
func ValidateRepositoryName(name string) error {
|
||||
if len(name) > RepositoryNameTotalLengthMax {
|
||||
return ErrRepositoryNameLong
|
||||
}
|
||||
|
|
|
@ -80,7 +80,7 @@ func TestRepositoryNameRegexp(t *testing.T) {
|
|||
t.Fail()
|
||||
}
|
||||
|
||||
if err := ValidateRespositoryName(testcase.input); err != testcase.err {
|
||||
if err := ValidateRepositoryName(testcase.input); err != testcase.err {
|
||||
if testcase.err != nil {
|
||||
if err != nil {
|
||||
failf("unexpected error for invalid repository: got %v, expected %v", err, testcase.err)
|
||||
|
|
|
@ -98,6 +98,7 @@ func TestRouter(t *testing.T) {
|
|||
},
|
||||
},
|
||||
{
|
||||
// support uuid proper
|
||||
RouteName: RouteNameBlobUploadChunk,
|
||||
RequestURI: "/v2/foo/bar/blobs/uploads/D95306FA-FAD3-4E36-8D41-CF1C93EF8286",
|
||||
Vars: map[string]string{
|
||||
|
@ -113,6 +114,21 @@ func TestRouter(t *testing.T) {
|
|||
"uuid": "RDk1MzA2RkEtRkFEMy00RTM2LThENDEtQ0YxQzkzRUY4Mjg2IA==",
|
||||
},
|
||||
},
|
||||
{
|
||||
// supports urlsafe base64
|
||||
RouteName: RouteNameBlobUploadChunk,
|
||||
RequestURI: "/v2/foo/bar/blobs/uploads/RDk1MzA2RkEtRkFEMy00RTM2LThENDEtQ0YxQzkzRUY4Mjg2IA_-==",
|
||||
Vars: map[string]string{
|
||||
"name": "foo/bar",
|
||||
"uuid": "RDk1MzA2RkEtRkFEMy00RTM2LThENDEtQ0YxQzkzRUY4Mjg2IA_-==",
|
||||
},
|
||||
},
|
||||
{
|
||||
// does not match
|
||||
RouteName: RouteNameBlobUploadChunk,
|
||||
RequestURI: "/v2/foo/bar/blobs/uploads/totalandcompletejunk++$$-==",
|
||||
StatusCode: http.StatusNotFound,
|
||||
},
|
||||
{
|
||||
// Check ambiguity: ensure we can distinguish between tags for
|
||||
// "foo/bar/image/image" and image for "foo/bar/image" with tag
|
||||
|
|
|
@ -62,7 +62,12 @@ func NewURLBuilderFromRequest(r *http.Request) *URLBuilder {
|
|||
host := r.Host
|
||||
forwardedHost := r.Header.Get("X-Forwarded-Host")
|
||||
if len(forwardedHost) > 0 {
|
||||
host = forwardedHost
|
||||
// According to the Apache mod_proxy docs, X-Forwarded-Host can be a
|
||||
// comma-separated list of hosts, to which each proxy appends the
|
||||
// requested host. We want to grab the first from this comma-separated
|
||||
// list.
|
||||
hosts := strings.SplitN(forwardedHost, ",", 2)
|
||||
host = strings.TrimSpace(hosts[0])
|
||||
}
|
||||
|
||||
basePath := routeDescriptorsMap[RouteNameBase].Path
|
||||
|
|
|
@ -151,6 +151,12 @@ func TestBuilderFromRequest(t *testing.T) {
|
|||
forwardedProtoHeader := make(http.Header, 1)
|
||||
forwardedProtoHeader.Set("X-Forwarded-Proto", "https")
|
||||
|
||||
forwardedHostHeader1 := make(http.Header, 1)
|
||||
forwardedHostHeader1.Set("X-Forwarded-Host", "first.example.com")
|
||||
|
||||
forwardedHostHeader2 := make(http.Header, 1)
|
||||
forwardedHostHeader2.Set("X-Forwarded-Host", "first.example.com, proxy1.example.com")
|
||||
|
||||
testRequests := []struct {
|
||||
request *http.Request
|
||||
base string
|
||||
|
@ -163,6 +169,14 @@ func TestBuilderFromRequest(t *testing.T) {
|
|||
request: &http.Request{URL: u, Host: u.Host, Header: forwardedProtoHeader},
|
||||
base: "https://example.com",
|
||||
},
|
||||
{
|
||||
request: &http.Request{URL: u, Host: u.Host, Header: forwardedHostHeader1},
|
||||
base: "http://first.example.com",
|
||||
},
|
||||
{
|
||||
request: &http.Request{URL: u, Host: u.Host, Header: forwardedHostHeader2},
|
||||
base: "http://first.example.com",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tr := range testRequests {
|
||||
|
|
Loading…
Add table
Reference in a new issue