Merge pull request #13576 from stevvooe/verify-digests
Properly verify manifests and layer digests on pull
This commit is contained in:
commit
274baf70bf
21 changed files with 1001 additions and 197 deletions
|
@ -8,92 +8,164 @@ 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) {
|
||||
sig, err := libtrust.ParsePrettySignature(manifestBytes, "signatures")
|
||||
// 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) {
|
||||
payload, keys, err := unpackSignedManifest(manifestBytes)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("error parsing payload: %s", err)
|
||||
return "", nil, false, fmt.Errorf("error unpacking manifest: %v", err)
|
||||
}
|
||||
|
||||
keys, err := sig.Verify()
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("error verifying payload: %s", err)
|
||||
}
|
||||
// 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.
|
||||
|
||||
payload, err := sig.Payload()
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("error retrieving payload: %s", err)
|
||||
}
|
||||
var localDigest digest.Digest
|
||||
|
||||
var manifestDigest digest.Digest
|
||||
// Verify the local digest, if present in ref. ParseDigest will validate
|
||||
// that the ref is a digest and verify against that if present. Otherwize
|
||||
// (on error), we simply compute the localDigest and proceed.
|
||||
if dgst, err := digest.ParseDigest(ref); err == nil {
|
||||
// verify the manifest against local ref
|
||||
if err := verifyDigest(dgst, payload); err != nil {
|
||||
return "", nil, false, fmt.Errorf("verifying local digest: %v", err)
|
||||
}
|
||||
|
||||
if dgst != "" {
|
||||
manifestDigest, err = digest.ParseDigest(dgst)
|
||||
localDigest = dgst
|
||||
} else {
|
||||
// We don't have a local digest, since we are working from a tag.
|
||||
// Compute the digest of the payload and return that.
|
||||
logrus.Debugf("provided manifest reference %q is not a digest: %v", ref, err)
|
||||
localDigest, err = digest.FromBytes(payload)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("invalid manifest digest from registry: %s", err)
|
||||
}
|
||||
|
||||
dgstVerifier, err := digest.NewDigestVerifier(manifestDigest)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("unable to verify manifest digest from registry: %s", 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)
|
||||
// near impossible
|
||||
logrus.Errorf("error calculating local digest during tag pull: %v", err)
|
||||
return "", nil, false, err
|
||||
}
|
||||
}
|
||||
|
||||
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 := verifyDigest(remoteDigest, payload); err != nil {
|
||||
return "", nil, false, fmt.Errorf("verifying remote digest: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
// validate the contents of the manifest
|
||||
if err := validateManifest(&manifest); err != nil {
|
||||
return "", nil, false, err
|
||||
}
|
||||
|
||||
var verified bool
|
||||
verified, err = s.verifyTrustedKeys(manifest.Name, keys)
|
||||
if err != nil {
|
||||
return "", nil, false, fmt.Errorf("error verifying trusted keys: %v", err)
|
||||
}
|
||||
|
||||
return localDigest, &manifest, verified, nil
|
||||
}
|
||||
|
||||
// unpackSignedManifest takes the raw, signed manifest bytes, unpacks the jws
|
||||
// and returns the payload and public keys used to signed the manifest.
|
||||
// Signatures are verified for authenticity but not against the trust store.
|
||||
func unpackSignedManifest(p []byte) ([]byte, []libtrust.PublicKey, error) {
|
||||
sig, err := libtrust.ParsePrettySignature(p, "signatures")
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("error parsing payload: %s", err)
|
||||
}
|
||||
|
||||
keys, err := sig.Verify()
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("error verifying payload: %s", err)
|
||||
}
|
||||
|
||||
payload, err := sig.Payload()
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("error retrieving payload: %s", err)
|
||||
}
|
||||
|
||||
return payload, keys, nil
|
||||
}
|
||||
|
||||
// verifyTrustedKeys checks the keys provided against the trust store,
|
||||
// ensuring that the provided keys are trusted for the namespace. The keys
|
||||
// provided from this method must come from the signatures provided as part of
|
||||
// the manifest JWS package, obtained from unpackSignedManifest or libtrust.
|
||||
func (s *TagStore) verifyTrustedKeys(namespace string, keys []libtrust.PublicKey) (verified bool, err error) {
|
||||
if namespace[0] != '/' {
|
||||
namespace = "/" + namespace
|
||||
}
|
||||
|
||||
for _, key := range keys {
|
||||
namespace := manifest.Name
|
||||
if namespace[0] != '/' {
|
||||
namespace = "/" + namespace
|
||||
}
|
||||
b, err := key.MarshalJSON()
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("error marshalling public key: %s", err)
|
||||
return 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 false, fmt.Errorf("error running key check: %s", err)
|
||||
}
|
||||
logrus.Debugf("Key check result: %v", vErr)
|
||||
}
|
||||
verified = v
|
||||
if verified {
|
||||
logrus.Debug("Key check result: verified")
|
||||
}
|
||||
}
|
||||
return &manifest, verified, nil
|
||||
|
||||
if verified {
|
||||
logrus.Debug("Key check result: verified")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func checkValidManifest(manifest *registry.ManifestData) error {
|
||||
// verifyDigest checks the contents of p against the provided digest. Note
|
||||
// that for manifests, this is the signed payload and not the raw bytes with
|
||||
// signatures.
|
||||
func verifyDigest(dgst digest.Digest, p []byte) error {
|
||||
if err := dgst.Validate(); err != nil {
|
||||
return fmt.Errorf("error validating digest %q: %v", dgst, err)
|
||||
}
|
||||
|
||||
verifier, err := digest.NewDigestVerifier(dgst)
|
||||
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 fmt.Errorf("error creating verifier for digest %q: %v", dgst, err)
|
||||
}
|
||||
|
||||
if _, err := verifier.Write(p); err != nil {
|
||||
return fmt.Errorf("error writing payload to digest verifier (verifier target %q): %v", dgst, err)
|
||||
}
|
||||
|
||||
if !verifier.Verified() {
|
||||
return fmt.Errorf("verification against digest %q failed", dgst)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateManifest(manifest *registry.ManifestData) error {
|
||||
if manifest.SchemaVersion != 1 {
|
||||
return fmt.Errorf("unsupported schema version: %d", manifest.SchemaVersion)
|
||||
}
|
||||
|
||||
if len(manifest.FSLayers) != len(manifest.History) {
|
||||
return fmt.Errorf("length of history not equal to number of layers")
|
||||
}
|
||||
|
|
|
@ -8,11 +8,13 @@ import (
|
|||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/distribution/digest"
|
||||
"github.com/docker/docker/image"
|
||||
"github.com/docker/docker/pkg/tarsum"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/docker/docker/runconfig"
|
||||
"github.com/docker/docker/utils"
|
||||
"github.com/docker/libtrust"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -181,3 +183,121 @@ func TestManifestTarsumCache(t *testing.T) {
|
|||
t.Fatalf("Unexpected json value\nExpected:\n%s\nActual:\n%s", v1compat, manifest.History[0].V1Compatibility)
|
||||
}
|
||||
}
|
||||
|
||||
// TestManifestDigestCheck ensures that loadManifest properly verifies the
|
||||
// remote and local digest.
|
||||
func TestManifestDigestCheck(t *testing.T) {
|
||||
tmp, err := utils.TestDirectory("")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tmp)
|
||||
store := mkTestTagStore(tmp, t)
|
||||
defer store.graph.driver.Cleanup()
|
||||
|
||||
archive, err := fakeTar()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
img := &image.Image{ID: testManifestImageID}
|
||||
if err := store.graph.Register(img, archive); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := store.Tag(testManifestImageName, testManifestTag, testManifestImageID, false); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if cs, err := img.GetCheckSum(store.graph.ImageRoot(testManifestImageID)); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if cs != "" {
|
||||
t.Fatalf("Non-empty checksum file after register")
|
||||
}
|
||||
|
||||
// Generate manifest
|
||||
payload, err := store.newManifest(testManifestImageName, testManifestImageName, testManifestTag)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error generating test manifest: %v", err)
|
||||
}
|
||||
|
||||
pk, err := libtrust.GenerateECP256PrivateKey()
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error generating private key: %v", err)
|
||||
}
|
||||
|
||||
sig, err := libtrust.NewJSONSignature(payload)
|
||||
if err != nil {
|
||||
t.Fatalf("error creating signature: %v", err)
|
||||
}
|
||||
|
||||
if err := sig.Sign(pk); err != nil {
|
||||
t.Fatalf("error signing manifest bytes: %v", err)
|
||||
}
|
||||
|
||||
signedBytes, err := sig.PrettySignature("signatures")
|
||||
if err != nil {
|
||||
t.Fatalf("error getting signed bytes: %v", err)
|
||||
}
|
||||
|
||||
dgst, err := digest.FromBytes(payload)
|
||||
if err != nil {
|
||||
t.Fatalf("error getting digest of manifest: %v", err)
|
||||
}
|
||||
|
||||
// use this as the "bad" digest
|
||||
zeroDigest, err := digest.FromBytes([]byte{})
|
||||
if err != nil {
|
||||
t.Fatalf("error making zero digest: %v", err)
|
||||
}
|
||||
|
||||
// Remote and local match, everything should look good
|
||||
local, _, _, err := store.loadManifest(signedBytes, dgst.String(), dgst)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error verifying local and remote digest: %v", err)
|
||||
}
|
||||
|
||||
if local != dgst {
|
||||
t.Fatalf("local digest not correctly calculated: %v", err)
|
||||
}
|
||||
|
||||
// remote and no local, since pulling by tag
|
||||
local, _, _, err = store.loadManifest(signedBytes, "tag", dgst)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error verifying tag pull and remote digest: %v", err)
|
||||
}
|
||||
|
||||
if local != dgst {
|
||||
t.Fatalf("local digest not correctly calculated: %v", err)
|
||||
}
|
||||
|
||||
// remote and differing local, this is the most important to fail
|
||||
local, _, _, err = store.loadManifest(signedBytes, zeroDigest.String(), dgst)
|
||||
if err == nil {
|
||||
t.Fatalf("error expected when verifying with differing local digest")
|
||||
}
|
||||
|
||||
// no remote, no local (by tag)
|
||||
local, _, _, err = store.loadManifest(signedBytes, "tag", "")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error verifying manifest without remote digest: %v", err)
|
||||
}
|
||||
|
||||
if local != dgst {
|
||||
t.Fatalf("local digest not correctly calculated: %v", err)
|
||||
}
|
||||
|
||||
// no remote, with local
|
||||
local, _, _, err = store.loadManifest(signedBytes, dgst.String(), "")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error verifying manifest without remote digest: %v", err)
|
||||
}
|
||||
|
||||
if local != dgst {
|
||||
t.Fatalf("local digest not correctly calculated: %v", err)
|
||||
}
|
||||
|
||||
// bad remote, we fail the check.
|
||||
local, _, _, err = store.loadManifest(signedBytes, dgst.String(), zeroDigest)
|
||||
if err == nil {
|
||||
t.Fatalf("error expected when verifying with differing remote digest")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -457,17 +457,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 {
|
||||
|
@ -517,27 +506,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-- {
|
||||
|
@ -610,8 +606,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))
|
||||
|
@ -688,15 +683,33 @@ 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 {
|
||||
out.Write(sf.FormatStatus("", "Digest: %s", localDigest))
|
||||
|
||||
if tag == localDigest.String() {
|
||||
// TODO(stevvooe): Ideally, we should always set the digest so we can
|
||||
// use the digest whether we pull by it or not. Unfortunately, the tag
|
||||
// store treats the digest as a separate tag, meaning there may be an
|
||||
// untagged digest image that would seem to be dangling by a user.
|
||||
|
||||
if err = s.SetDigest(repoInfo.LocalName, localDigest.String(), downloads[0].img.ID); err != nil {
|
||||
return false, err
|
||||
}
|
||||
} else {
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@ import (
|
|||
"github.com/docker/docker/daemon/graphdriver"
|
||||
_ "github.com/docker/docker/daemon/graphdriver/vfs" // import the vfs driver so it is used in the tests
|
||||
"github.com/docker/docker/image"
|
||||
"github.com/docker/docker/trust"
|
||||
"github.com/docker/docker/utils"
|
||||
)
|
||||
|
||||
|
@ -60,9 +61,16 @@ func mkTestTagStore(root string, t *testing.T) *TagStore {
|
|||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
trust, err := trust.NewTrustStore(root + "/trust")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
tagCfg := &TagStoreConfig{
|
||||
Graph: graph,
|
||||
Events: events.New(),
|
||||
Trust: trust,
|
||||
}
|
||||
store, err := NewTagStore(path.Join(root, "tags"), tagCfg)
|
||||
if err != nil {
|
||||
|
|
|
@ -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…
Reference in a new issue