diff --git a/.cirrus.yml b/.cirrus.yml index 7d44c28be9..5287ff5004 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -32,10 +32,10 @@ env: #### #### Cache-image names to test with (double-quotes around names are critical) #### - FEDORA_NAME: "fedora-36" + FEDORA_NAME: "fedora-37" ### c20230120t152650z-f37f36u2204 # Google-cloud VM Images - IMAGE_SUFFIX: "c5495735033528320" + IMAGE_SUFFIX: "c20230120t152650z-f37f36u2204" FEDORA_CACHE_IMAGE_NAME: "fedora-${IMAGE_SUFFIX}" # Container FQIN's (include bleeding-edge development-level container deps.) diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index 82320ecca7..0000000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,9 +0,0 @@ -version: 2 -updates: -- package-ecosystem: gomod - directory: "/" - schedule: - interval: daily - time: "10:00" - timezone: Europe/Berlin - open-pull-requests-limit: 10 diff --git a/.github/renovate.json5 b/.github/renovate.json5 new file mode 100644 index 0000000000..22f80d3921 --- /dev/null +++ b/.github/renovate.json5 @@ -0,0 +1,55 @@ +/* + Renovate is a service similar to GitHub Dependabot, but with + (fantastically) more configuration options. So many options + in fact, if you're new I recommend glossing over this cheat-sheet + prior to the official documentation: + + https://www.augmentedmind.de/2021/07/25/renovate-bot-cheat-sheet + + Configuration Update/Change Procedure: + 1. Make changes + 2. Manually validate changes (from repo-root): + + podman run -it \ + -v ./.github/renovate.json5:/usr/src/app/renovate.json5:z \ + docker.io/renovate/renovate:latest \ + renovate-config-validator + 3. Commit. + + Configuration Reference: + https://docs.renovatebot.com/configuration-options/ + + Monitoring Dashboard: + https://app.renovatebot.com/dashboard#github/containers + + Note: The Renovate bot will create/manage it's business on + branches named 'renovate/*'. Otherwise, and by + default, the only the copy of this file that matters + is the one on the `main` branch. No other branches + will be monitored or touched in any way. +*/ + +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + + /************************************************* + ****** Global/general configuration options ***** + *************************************************/ + + // Re-use predefined sets of configuration options to DRY + "extends": [ + // https://github.com/containers/automation/blob/main/renovate/defaults.json5 + "github>containers/automation//renovate/defaults.json5" + ], + + // Permit automatic rebasing when base-branch changes by more than + // one commit. + "rebaseWhen": "behind-base-branch", + + /************************************************* + *** Repository-specific configuration options *** + *************************************************/ + + // Don't leave dep. update. PRs "hanging", assign them to people. + "assignees": ["containers/image-maintainers"], +} diff --git a/Makefile b/Makefile index 24a6a4a92a..fe88f1d108 100644 --- a/Makefile +++ b/Makefile @@ -55,7 +55,7 @@ tools: .install.gitvalidation .install.golangci-lint .install.golint .install.golangci-lint: if [ ! -x "$(GOBIN)/golangci-lint" ]; then \ - curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh| sh -s -- -b $(GOBIN) v1.49.0; \ + curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh| sh -s -- -b $(GOBIN) v1.51.0; \ fi .install.golint: @@ -85,3 +85,9 @@ lint: git fetch -q "https://github.com/containers/image.git" "refs/heads/main" upstream="$$(git rev-parse --verify FETCH_HEAD)" ; \ $(GOBIN)/git-validation -q -run DCO,short-subject,dangling-whitespace -range $$upstream..HEAD + +vendor-in-container: + podman run --privileged --rm --env HOME=/root -v `pwd`:/src -w /src golang go mod tidy + +codespell: + codespell -S Makefile,build,buildah,buildah.spec,imgtype,copy,AUTHORS,bin,vendor,.git,go.sum,CHANGELOG.md,changelog.txt,seccomp.json,.cirrus.yml,"*.xz,*.gz,*.tar,*.tgz,*ico,*.png,*.1,*.5,*.orig,*.rej" -L keypair,flate,uint,iff,od,ERRO -w diff --git a/README.md b/README.md index 85d152da83..954603b74e 100644 --- a/README.md +++ b/README.md @@ -57,10 +57,12 @@ or use the build tags described below to avoid the dependencies (e.g. using `go ### Supported build tags +- `containers_image_docker_daemon_stub`: Don’t import the `docker-daemon:` transport in `github.com/containers/image/transports/alltransports`, to decrease the amount of required dependencies. Use a stub which reports that the transport is not supported instead. - `containers_image_openpgp`: Use a Golang-only OpenPGP implementation for signature verification instead of the default cgo/gpgme-based implementation; the primary downside is that creating new signatures with the Golang-only implementation is not supported. - `containers_image_ostree`: Import `ostree:` transport in `github.com/containers/image/transports/alltransports`. This builds the library requiring the `libostree` development libraries. Otherwise a stub which reports that the transport is not supported gets used. The `github.com/containers/image/ostree` package is completely disabled and impossible to import when this build tag is not in use. +- `containers_image_storage_stub`: Don’t import the `containers-storage:` transport in `github.com/containers/image/transports/alltransports`, to decrease the amount of required dependencies. Use a stub which reports that the transport is not supported instead. ## [Contributing](CONTRIBUTING.md) diff --git a/copy/compression.go b/copy/compression.go index ff0e7945dd..8960894292 100644 --- a/copy/compression.go +++ b/copy/compression.go @@ -10,6 +10,7 @@ import ( compressiontypes "github.com/containers/image/v5/pkg/compression/types" "github.com/containers/image/v5/types" "github.com/sirupsen/logrus" + "golang.org/x/exp/maps" ) // bpDetectCompressionStepData contains data that the copy pipeline needs about the “detect compression” step. @@ -199,7 +200,9 @@ func (ic *imageCopier) bpcDecompressCompressed(stream *sourceStream, detected bp } // bpcPreserveOriginal returns a *bpCompressionStepData for not changing the original blob. -func (ic *imageCopier) bpcPreserveOriginal(stream *sourceStream, detected bpDetectCompressionStepData, +// This does not change the sourceStream parameter; we include it for symmetry with other +// pipeline steps. +func (ic *imageCopier) bpcPreserveOriginal(_ *sourceStream, detected bpDetectCompressionStepData, layerCompressionChangeSupported bool) *bpCompressionStepData { logrus.Debugf("Using original blob without modification") // Remember if the original blob was compressed, and if so how, so that if @@ -232,13 +235,11 @@ func (d *bpCompressionStepData) updateCompressionEdits(operation *types.LayerCom if *annotations == nil { *annotations = map[string]string{} } - for k, v := range d.uploadedAnnotations { - (*annotations)[k] = v - } + maps.Copy(*annotations, d.uploadedAnnotations) } // recordValidatedBlobData updates b.blobInfoCache with data about the created uploadedInfo adnd the original srcInfo. -// This must ONLY be called if all data has been validated by OUR code, and is not comming from third parties. +// This must ONLY be called if all data has been validated by OUR code, and is not coming from third parties. func (d *bpCompressionStepData) recordValidatedDigestData(c *copier, uploadedInfo types.BlobInfo, srcInfo types.BlobInfo, encryptionStep *bpEncryptionStepData, decryptionStep *bpDecryptionStepData) error { // Don’t record any associations that involve encrypted data. This is a bit crude, diff --git a/copy/copy.go b/copy/copy.go index 6758d4de13..a2125d7ed8 100644 --- a/copy/copy.go +++ b/copy/copy.go @@ -17,20 +17,24 @@ import ( "github.com/containers/image/v5/internal/image" "github.com/containers/image/v5/internal/imagedestination" "github.com/containers/image/v5/internal/imagesource" + internalManifest "github.com/containers/image/v5/internal/manifest" "github.com/containers/image/v5/internal/pkg/platform" "github.com/containers/image/v5/internal/private" + "github.com/containers/image/v5/internal/set" "github.com/containers/image/v5/manifest" "github.com/containers/image/v5/pkg/blobinfocache" "github.com/containers/image/v5/pkg/compression" compressiontypes "github.com/containers/image/v5/pkg/compression/types" "github.com/containers/image/v5/signature" + "github.com/containers/image/v5/signature/signer" "github.com/containers/image/v5/transports" "github.com/containers/image/v5/types" encconfig "github.com/containers/ocicrypt/config" digest "github.com/opencontainers/go-digest" imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" "github.com/sirupsen/logrus" - "github.com/vbauerster/mpb/v7" + "github.com/vbauerster/mpb/v8" + "golang.org/x/exp/slices" "golang.org/x/sync/semaphore" "golang.org/x/term" ) @@ -61,6 +65,7 @@ var expectedCompressionFormats = map[string]*compressiontypes.Algorithm{ // copier allows us to keep track of diffID values for blobs, and other // data shared across one or more images in a possible manifest list. +// The owner must call close() when done. type copier struct { dest private.ImageDestination rawSource private.ImageSource @@ -75,6 +80,8 @@ type copier struct { ociEncryptConfig *encconfig.EncryptConfig concurrentBlobCopiesSemaphore *semaphore.Weighted // Limits the amount of concurrently copied blobs downloadForeignLayers bool + signers []*signer.Signer // Signers to use to create new signatures for the image + signersToClose []*signer.Signer // Signers that should be closed when this copier is destroyed. } // imageCopier tracks state specific to a single image (possibly an item of a manifest list) @@ -121,21 +128,25 @@ type ImageListSelection int // Options allows supplying non-default configuration modifying the behavior of CopyImage. type Options struct { - RemoveSignatures bool // Remove any pre-existing signatures. SignBy will still add a new signature. + RemoveSignatures bool // Remove any pre-existing signatures. Signers and SignBy… will still add a new signature. + // Signers to use to add signatures during the copy. + // Callers are still responsible for closing these Signer objects; they can be reused for multiple copy.Image operations in a row. + Signers []*signer.Signer SignBy string // If non-empty, asks for a signature to be added during the copy, and specifies a key ID, as accepted by signature.NewGPGSigningMechanism().SignDockerManifest(), - SignPassphrase string // Passphare to use when signing with the key ID from `SignBy`. + SignPassphrase string // Passphrase to use when signing with the key ID from `SignBy`. SignBySigstorePrivateKeyFile string // If non-empty, asks for a signature to be added during the copy, using a sigstore private key file at the provided path. - SignSigstorePrivateKeyPassphrase []byte // Passphare to use when signing with `SignBySigstorePrivateKeyFile`. + SignSigstorePrivateKeyPassphrase []byte // Passphrase to use when signing with `SignBySigstorePrivateKeyFile`. SignIdentity reference.Named // Identify to use when signing, defaults to the docker reference of the destination - ReportWriter io.Writer - SourceCtx *types.SystemContext - DestinationCtx *types.SystemContext - ProgressInterval time.Duration // time to wait between reports to signal the progress channel - Progress chan types.ProgressProperties // Reported to when ProgressInterval has arrived for a single artifact+offset. + + ReportWriter io.Writer + SourceCtx *types.SystemContext + DestinationCtx *types.SystemContext + ProgressInterval time.Duration // time to wait between reports to signal the progress channel + Progress chan types.ProgressProperties // Reported to when ProgressInterval has arrived for a single artifact+offset. // Preserve digests, and fail if we cannot. PreserveDigests bool - // manifest MIME type of image set by user. "" is default and means use the autodetection to the the manifest MIME type + // manifest MIME type of image set by user. "" is default and means use the autodetection to the manifest MIME type ForceManifestMIMEType string ImageListSelection ImageListSelection // set to either CopySystemImage (the default), CopyAllImages, or CopySpecificImages to control which instances we copy when the source reference is a list; ignored if the source reference is not a list Instances []digest.Digest // if ImageListSelection is CopySpecificImages, copy only these instances and the list itself @@ -257,6 +268,7 @@ func Image(ctx context.Context, policyContext *signature.PolicyContext, destRef, ociEncryptConfig: options.OciEncryptConfig, downloadForeignLayers: options.DownloadForeignLayers, } + defer c.close() // Set the concurrentBlobCopiesSemaphore if we can copy layers in parallel. if dest.HasThreadSafePutBlob() && rawSource.HasThreadSafeGetBlob() { @@ -284,6 +296,10 @@ func Image(ctx context.Context, policyContext *signature.PolicyContext, destRef, c.compressionLevel = options.DestinationCtx.CompressionLevel } + if err := c.setupSigners(options); err != nil { + return nil, err + } + unparsedToplevel := image.UnparsedInstance(rawSource, nil) multiImage, err := isMultiImage(ctx, unparsedToplevel) if err != nil { @@ -302,7 +318,7 @@ func Image(ctx context.Context, policyContext *signature.PolicyContext, destRef, if err != nil { return nil, fmt.Errorf("reading manifest for %s: %w", transports.ImageName(srcRef), err) } - manifestList, err := manifest.ListFromBlob(mfest, manifestType) + manifestList, err := internalManifest.ListFromBlob(mfest, manifestType) if err != nil { return nil, fmt.Errorf("parsing primary manifest as list for %s: %w", transports.ImageName(srcRef), err) } @@ -340,6 +356,15 @@ func Image(ctx context.Context, policyContext *signature.PolicyContext, destRef, return copiedManifest, nil } +// close tears down state owned by copier. +func (c *copier) close() { + for i, s := range c.signersToClose { + if err := s.Close(); err != nil { + logrus.Warnf("Error closing per-copy signer %d: %v", i+1, err) + } + } +} + // Checks if the destination supports accepting multiple images by checking if it can support // manifest types that are lists of other manifests. func supportsMultipleImages(dest types.ImageDestination) bool { @@ -348,12 +373,7 @@ func supportsMultipleImages(dest types.ImageDestination) bool { // Anything goes! return true } - for _, mtype := range mtypes { - if manifest.MIMETypeIsMultiImage(mtype) { - return true - } - } - return false + return slices.ContainsFunc(mtypes, manifest.MIMETypeIsMultiImage) } // compareImageDestinationManifestEqual compares the `src` and `dest` image manifests (reading the manifest from the @@ -398,11 +418,11 @@ func (c *copier) copyMultipleImages(ctx context.Context, policyContext *signatur if err != nil { return nil, fmt.Errorf("reading manifest list: %w", err) } - originalList, err := manifest.ListFromBlob(manifestList, manifestType) + originalList, err := internalManifest.ListFromBlob(manifestList, manifestType) if err != nil { return nil, fmt.Errorf("parsing manifest list %q: %w", string(manifestList), err) } - updatedList := originalList.Clone() + updatedList := originalList.CloneInternal() sigs, err := c.sourceSignatures(ctx, unparsedToplevel, options, "Getting image list signatures", @@ -469,24 +489,16 @@ func (c *copier) copyMultipleImages(ctx context.Context, policyContext *signatur updates := make([]manifest.ListUpdate, len(instanceDigests)) instancesCopied := 0 for i, instanceDigest := range instanceDigests { - if options.ImageListSelection == CopySpecificImages { - skip := true - for _, instance := range options.Instances { - if instance == instanceDigest { - skip = false - break - } - } - if skip { - update, err := updatedList.Instance(instanceDigest) - if err != nil { - return nil, err - } - logrus.Debugf("Skipping instance %s (%d/%d)", instanceDigest, i+1, len(instanceDigests)) - // Record the digest/size/type of the manifest that we didn't copy. - updates[i] = update - continue + if options.ImageListSelection == CopySpecificImages && + !slices.Contains(options.Instances, instanceDigest) { + update, err := updatedList.Instance(instanceDigest) + if err != nil { + return nil, err } + logrus.Debugf("Skipping instance %s (%d/%d)", instanceDigest, i+1, len(instanceDigests)) + // Record the digest/size/type of the manifest that we didn't copy. + updates[i] = update + continue } logrus.Debugf("Copying instance %s (%d/%d)", instanceDigest, i+1, len(instanceDigests)) c.Printf("Copying image %s (%d/%d)\n", instanceDigest, instancesCopied+1, imagesToCopy) @@ -514,7 +526,7 @@ func (c *copier) copyMultipleImages(ctx context.Context, policyContext *signatur c.Printf("Writing manifest list to image destination\n") var errs []string for _, thisListType := range append([]string{selectedListType}, otherManifestMIMETypeCandidates...) { - attemptedList := updatedList + var attemptedList internalManifest.ListPublic = updatedList logrus.Debugf("Trying to use manifest list type %s…", thisListType) @@ -564,20 +576,11 @@ func (c *copier) copyMultipleImages(ctx context.Context, policyContext *signatur } // Sign the manifest list. - if options.SignBy != "" { - newSig, err := c.createSignature(manifestList, options.SignBy, options.SignPassphrase, options.SignIdentity) - if err != nil { - return nil, err - } - sigs = append(sigs, newSig) - } - if options.SignBySigstorePrivateKeyFile != "" { - newSig, err := c.createSigstoreSignature(manifestList, options.SignBySigstorePrivateKeyFile, options.SignSigstorePrivateKeyPassphrase, options.SignIdentity) - if err != nil { - return nil, err - } - sigs = append(sigs, newSig) + newSigs, err := c.createSignatures(ctx, manifestList, options.SignIdentity) + if err != nil { + return nil, err } + sigs = append(sigs, newSigs...) c.Printf("Storing list signatures\n") if err := c.dest.PutSignaturesWithFormat(ctx, sigs, nil); err != nil { @@ -675,12 +678,12 @@ func (c *copier) copyOneImage(ctx context.Context, policyContext *signature.Poli // Decide whether we can substitute blobs with semantic equivalents: // - Don’t do that if we can’t modify the manifest at all // - Ensure _this_ copy sees exactly the intended data when either processing a signed image or signing it. - // This may be too conservative, but for now, better safe than sorry, _especially_ on the SignBy path: + // This may be too conservative, but for now, better safe than sorry, _especially_ on the len(c.signers) != 0 path: // The signature makes the content non-repudiable, so it very much matters that the signature is made over exactly what the user intended. // We do intend the RecordDigestUncompressedPair calls to only work with reliable data, but at least there’s a risk // that the compressed version coming from a third party may be designed to attack some other decompressor implementation, // and we would reuse and sign it. - ic.canSubstituteBlobs = ic.cannotModifyManifestReason == "" && options.SignBy == "" && options.SignBySigstorePrivateKeyFile == "" + ic.canSubstituteBlobs = ic.cannotModifyManifestReason == "" && len(c.signers) == 0 if err := ic.updateEmbeddedDockerReference(); err != nil { return nil, "", "", err @@ -711,7 +714,7 @@ func (c *copier) copyOneImage(ctx context.Context, policyContext *signature.Poli // If enabled, fetch and compare the destination's manifest. And as an optimization skip updating the destination iff equal if options.OptimizeDestinationImageAlreadyExists { - shouldUpdateSigs := len(sigs) > 0 || options.SignBy != "" || options.SignBySigstorePrivateKeyFile != "" // TODO: Consider allowing signatures updates only and skipping the image's layers/manifest copy if possible + shouldUpdateSigs := len(sigs) > 0 || len(c.signers) != 0 // TODO: Consider allowing signatures updates only and skipping the image's layers/manifest copy if possible noPendingManifestUpdates := ic.noPendingManifestUpdates() logrus.Debugf("Checking if we can skip copying: has signatures=%t, OCI encryption=%t, no manifest updates=%t", shouldUpdateSigs, destRequiresOciEncryption, noPendingManifestUpdates) @@ -791,20 +794,11 @@ func (c *copier) copyOneImage(ctx context.Context, policyContext *signature.Poli targetInstance = &retManifestDigest } - if options.SignBy != "" { - newSig, err := c.createSignature(manifestBytes, options.SignBy, options.SignPassphrase, options.SignIdentity) - if err != nil { - return nil, "", "", err - } - sigs = append(sigs, newSig) - } - if options.SignBySigstorePrivateKeyFile != "" { - newSig, err := c.createSigstoreSignature(manifestBytes, options.SignBySigstorePrivateKeyFile, options.SignSigstorePrivateKeyPassphrase, options.SignIdentity) - if err != nil { - return nil, "", "", err - } - sigs = append(sigs, newSig) + newSigs, err := c.createSignatures(ctx, manifestBytes, options.SignIdentity) + if err != nil { + return nil, "", "", err } + sigs = append(sigs, newSigs...) c.Printf("Storing signatures\n") if err := c.dest.PutSignaturesWithFormat(ctx, sigs, targetInstance); err != nil { @@ -819,7 +813,7 @@ func (c *copier) copyOneImage(ctx context.Context, policyContext *signature.Poli // has a built-in list of functions/methods (whatever object they are for) // which have their format strings checked; for other names we would have // to pass a parameter to every (go tool vet) invocation. -func (c *copier) Printf(format string, a ...interface{}) { +func (c *copier) Printf(format string, a ...any) { fmt.Fprintf(c.reportWriter, format, a...) } @@ -944,20 +938,20 @@ func (ic *imageCopier) copyLayers(ctx context.Context) error { data[index] = cld } - // Create layer Encryption map - encLayerBitmap := map[int]bool{} + // Decide which layers to encrypt + layersToEncrypt := set.New[int]() var encryptAll bool if ic.ociEncryptLayers != nil { encryptAll = len(*ic.ociEncryptLayers) == 0 totalLayers := len(srcInfos) for _, l := range *ic.ociEncryptLayers { // if layer is negative, it is reverse indexed. - encLayerBitmap[(totalLayers+l)%totalLayers] = true + layersToEncrypt.Add((totalLayers + l) % totalLayers) } if encryptAll { for i := 0; i < len(srcInfos); i++ { - encLayerBitmap[i] = true + layersToEncrypt.Add(i) } } } @@ -976,7 +970,7 @@ func (ic *imageCopier) copyLayers(ctx context.Context) error { return fmt.Errorf("copying layer: %w", err) } copyGroup.Add(1) - go copyLayerHelper(i, srcLayer, encLayerBitmap[i], progressPool, ic.c.rawSource.Reference().DockerReference()) + go copyLayerHelper(i, srcLayer, layersToEncrypt.Contains(i), progressPool, ic.c.rawSource.Reference().DockerReference()) } // A call to copyGroup.Wait() is done at this point by the defer above. @@ -1009,15 +1003,9 @@ func (ic *imageCopier) copyLayers(ctx context.Context) error { // layerDigestsDiffer returns true iff the digests in a and b differ (ignoring sizes and possible other fields) func layerDigestsDiffer(a, b []types.BlobInfo) bool { - if len(a) != len(b) { - return true - } - for i := range a { - if a[i].Digest != b[i].Digest { - return true - } - } - return false + return !slices.EqualFunc(a, b, func(a, b types.BlobInfo) bool { + return a.Digest == b.Digest + }) } // copyUpdatedConfigAndManifest updates the image per ic.manifestUpdates, if necessary, diff --git a/copy/copy_test.go b/copy/copy_test.go index 7705ecab07..e9ad5a03d1 100644 --- a/copy/copy_test.go +++ b/copy/copy_test.go @@ -17,7 +17,7 @@ import ( func goDiffIDComputationGoroutineWithTimeout(layerStream io.ReadCloser, decompressor compressiontypes.DecompressorFunc) *diffIDResult { ch := make(chan diffIDResult) - go diffIDComputationGoroutine(ch, layerStream, nil) + go diffIDComputationGoroutine(ch, layerStream, decompressor) timeout := time.After(time.Second) select { case res := <-ch: diff --git a/copy/encryption.go b/copy/encryption.go index 5eae8bfcda..54aca9e572 100644 --- a/copy/encryption.go +++ b/copy/encryption.go @@ -7,6 +7,8 @@ import ( "github.com/containers/image/v5/types" "github.com/containers/ocicrypt" imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" + "golang.org/x/exp/maps" + "golang.org/x/exp/slices" ) // isOciEncrypted returns a bool indicating if a mediatype is encrypted @@ -18,12 +20,9 @@ func isOciEncrypted(mediatype string) bool { // isEncrypted checks if an image is encrypted func isEncrypted(i types.Image) bool { layers := i.LayerInfos() - for _, l := range layers { - if isOciEncrypted(l.MediaType) { - return true - } - } - return false + return slices.ContainsFunc(layers, func(l types.BlobInfo) bool { + return isOciEncrypted(l.MediaType) + }) } // bpDecryptionStepData contains data that the copy pipeline needs about the decryption step. @@ -47,11 +46,9 @@ func (c *copier) blobPipelineDecryptionStep(stream *sourceStream, srcInfo types. stream.reader = reader stream.info.Digest = decryptedDigest stream.info.Size = -1 - for k := range stream.info.Annotations { - if strings.HasPrefix(k, "org.opencontainers.image.enc") { - delete(stream.info.Annotations, k) - } - } + maps.DeleteFunc(stream.info.Annotations, func(k string, _ string) bool { + return strings.HasPrefix(k, "org.opencontainers.image.enc") + }) return &bpDecryptionStepData{ decrypting: true, }, nil @@ -122,8 +119,6 @@ func (d *bpEncryptionStepData) updateCryptoOperationAndAnnotations(operation *ty if *annotations == nil { *annotations = map[string]string{} } - for k, v := range encryptAnnotations { - (*annotations)[k] = v - } + maps.Copy(*annotations, encryptAnnotations) return nil } diff --git a/copy/manifest.go b/copy/manifest.go index df12e50c0f..a35ea42205 100644 --- a/copy/manifest.go +++ b/copy/manifest.go @@ -6,9 +6,11 @@ import ( "fmt" "strings" + "github.com/containers/image/v5/internal/set" "github.com/containers/image/v5/manifest" "github.com/containers/image/v5/types" "github.com/sirupsen/logrus" + "golang.org/x/exp/slices" ) // preferredManifestMIMETypes lists manifest MIME types in order of our preference, if we can't use the original manifest and need to convert. @@ -19,7 +21,7 @@ var preferredManifestMIMETypes = []string{manifest.DockerV2Schema2MediaType, man // orderedSet is a list of strings (MIME types or platform descriptors in our case), with each string appearing at most once. type orderedSet struct { list []string - included map[string]struct{} + included *set.Set[string] } // newOrderedSet creates a correctly initialized orderedSet. @@ -27,15 +29,15 @@ type orderedSet struct { func newOrderedSet() *orderedSet { return &orderedSet{ list: []string{}, - included: map[string]struct{}{}, + included: set.New[string](), } } // append adds s to the end of os, only if it is not included already. func (os *orderedSet) append(s string) { - if _, ok := os.included[s]; !ok { + if !os.included.Contains(s) { os.list = append(os.list, s) - os.included[s] = struct{}{} + os.included.Add(s) } } @@ -80,10 +82,10 @@ func determineManifestConversion(in determineManifestConversionInputs) (manifest otherMIMETypeCandidates: []string{}, }, nil } - supportedByDest := map[string]struct{}{} + supportedByDest := set.New[string]() for _, t := range destSupportedManifestMIMETypes { if !in.requiresOCIEncryption || manifest.MIMETypeSupportsEncryption(t) { - supportedByDest[t] = struct{}{} + supportedByDest.Add(t) } } @@ -96,7 +98,7 @@ func determineManifestConversion(in determineManifestConversionInputs) (manifest prioritizedTypes := newOrderedSet() // First of all, prefer to keep the original manifest unmodified. - if _, ok := supportedByDest[srcType]; ok { + if supportedByDest.Contains(srcType) { prioritizedTypes.append(srcType) } if in.cannotModifyManifestReason != "" { @@ -113,7 +115,7 @@ func determineManifestConversion(in determineManifestConversionInputs) (manifest // Then use our list of preferred types. for _, t := range preferredManifestMIMETypes { - if _, ok := supportedByDest[t]; ok { + if supportedByDest.Contains(t) { prioritizedTypes.append(t) } } @@ -166,11 +168,8 @@ func (c *copier) determineListConversion(currentListMIMEType string, destSupport prioritizedTypes := newOrderedSet() // The first priority is the current type, if it's in the list, since that lets us avoid a // conversion that isn't strictly necessary. - for _, t := range destSupportedMIMETypes { - if t == currentListMIMEType { - prioritizedTypes.append(currentListMIMEType) - break - } + if slices.Contains(destSupportedMIMETypes, currentListMIMEType) { + prioritizedTypes.append(currentListMIMEType) } // Pick out the other list types that we support. for _, t := range destSupportedMIMETypes { diff --git a/copy/progress_bars.go b/copy/progress_bars.go index 85676f01c6..25f2463630 100644 --- a/copy/progress_bars.go +++ b/copy/progress_bars.go @@ -7,8 +7,8 @@ import ( "github.com/containers/image/v5/internal/private" "github.com/containers/image/v5/types" - "github.com/vbauerster/mpb/v7" - "github.com/vbauerster/mpb/v7/decor" + "github.com/vbauerster/mpb/v8" + "github.com/vbauerster/mpb/v8/decor" ) // newProgressPool creates a *mpb.Progress. @@ -120,7 +120,7 @@ func (bar *progressBar) mark100PercentComplete() { bar.SetCurrent(bar.originalSize) // This triggers the completion condition. } else { // -1 = unknown size - // 0 is somewhat of a a special case: Unlike c/image, where 0 is a definite known + // 0 is somewhat of a special case: Unlike c/image, where 0 is a definite known // size (possible at least in theory), in mpb, zero-sized progress bars are treated // as unknown size, in particular they are not configured to be marked as // complete on bar.Current() reaching bar.total (because that would happen already diff --git a/copy/sign.go b/copy/sign.go index 6c3d9d62ca..fd19e18cd5 100644 --- a/copy/sign.go +++ b/copy/sign.go @@ -7,11 +7,49 @@ import ( "github.com/containers/image/v5/docker/reference" "github.com/containers/image/v5/internal/private" internalsig "github.com/containers/image/v5/internal/signature" - "github.com/containers/image/v5/signature" + internalSigner "github.com/containers/image/v5/internal/signer" "github.com/containers/image/v5/signature/sigstore" + "github.com/containers/image/v5/signature/simplesigning" "github.com/containers/image/v5/transports" ) +// setupSigners initializes c.signers based on options. +func (c *copier) setupSigners(options *Options) error { + c.signers = append(c.signers, options.Signers...) + // c.signersToClose is intentionally not updated with options.Signers. + + // We immediately append created signers to c.signers, and we rely on c.close() to clean them up; so we don’t need + // to clean up any created signers on failure. + + if options.SignBy != "" { + opts := []simplesigning.Option{ + simplesigning.WithKeyFingerprint(options.SignBy), + } + if options.SignPassphrase != "" { + opts = append(opts, simplesigning.WithPassphrase(options.SignPassphrase)) + } + signer, err := simplesigning.NewSigner(opts...) + if err != nil { + return err + } + c.signers = append(c.signers, signer) + c.signersToClose = append(c.signersToClose, signer) + } + + if options.SignBySigstorePrivateKeyFile != "" { + signer, err := sigstore.NewSigner( + sigstore.WithPrivateKeyFile(options.SignBySigstorePrivateKeyFile, options.SignSigstorePrivateKeyPassphrase), + ) + if err != nil { + return err + } + c.signers = append(c.signers, signer) + c.signersToClose = append(c.signersToClose, signer) + } + + return nil +} + // sourceSignatures returns signatures from unparsedSource based on options, // and verifies that they can be used (to avoid copying a large image when we // can tell in advance that it would ultimately fail) @@ -37,20 +75,16 @@ func (c *copier) sourceSignatures(ctx context.Context, unparsed private.Unparsed return sigs, nil } -// createSignature creates a new signature of manifest using keyIdentity. -func (c *copier) createSignature(manifest []byte, keyIdentity string, passphrase string, identity reference.Named) (internalsig.Signature, error) { - mech, err := signature.NewGPGSigningMechanism() - if err != nil { - return nil, fmt.Errorf("initializing GPG: %w", err) - } - defer mech.Close() - if err := mech.SupportsSigning(); err != nil { - return nil, fmt.Errorf("Signing not supported: %w", err) +// createSignatures creates signatures for manifest and an optional identity. +func (c *copier) createSignatures(ctx context.Context, manifest []byte, identity reference.Named) ([]internalsig.Signature, error) { + if len(c.signers) == 0 { + // We must exit early here, otherwise copies with no Docker reference wouldn’t be possible. + return nil, nil } if identity != nil { if reference.IsNameOnly(identity) { - return nil, fmt.Errorf("Sign identity must be a fully specified reference %s", identity) + return nil, fmt.Errorf("Sign identity must be a fully specified reference %s", identity.String()) } } else { identity = c.dest.Reference().DockerReference() @@ -59,31 +93,23 @@ func (c *copier) createSignature(manifest []byte, keyIdentity string, passphrase } } - c.Printf("Signing manifest using simple signing\n") - newSig, err := signature.SignDockerManifestWithOptions(manifest, identity.String(), mech, keyIdentity, &signature.SignOptions{Passphrase: passphrase}) - if err != nil { - return nil, fmt.Errorf("creating signature: %w", err) - } - return internalsig.SimpleSigningFromBlob(newSig), nil -} - -// createSigstoreSignature creates a new sigstore signature of manifest using privateKeyFile and identity. -func (c *copier) createSigstoreSignature(manifest []byte, privateKeyFile string, passphrase []byte, identity reference.Named) (internalsig.Signature, error) { - if identity != nil { - if reference.IsNameOnly(identity) { - return nil, fmt.Errorf("Sign identity must be a fully specified reference %s", identity.String()) + res := make([]internalsig.Signature, 0, len(c.signers)) + for signerIndex, signer := range c.signers { + msg := internalSigner.ProgressMessage(signer) + if len(c.signers) == 1 { + c.Printf("Creating signature: %s\n", msg) + } else { + c.Printf("Creating signature %d: %s\n", signerIndex+1, msg) } - } else { - identity = c.dest.Reference().DockerReference() - if identity == nil { - return nil, fmt.Errorf("Cannot determine canonical Docker reference for destination %s", transports.ImageName(c.dest.Reference())) + newSig, err := internalSigner.SignImageManifest(ctx, signer, manifest, identity) + if err != nil { + if len(c.signers) == 1 { + return nil, fmt.Errorf("creating signature: %w", err) + } else { + return nil, fmt.Errorf("creating signature %d: %w", signerIndex, err) + } } + res = append(res, newSig) } - - c.Printf("Signing manifest using a sigstore signature\n") - newSig, err := sigstore.SignDockerManifestWithPrivateKeyFileUnstable(manifest, identity, privateKeyFile, passphrase) - if err != nil { - return nil, fmt.Errorf("creating signature: %w", err) - } - return newSig, nil + return res, nil } diff --git a/copy/sign_test.go b/copy/sign_test.go index ef953fea75..c0733dee83 100644 --- a/copy/sign_test.go +++ b/copy/sign_test.go @@ -2,6 +2,7 @@ package copy import ( "context" + "errors" "io" "testing" @@ -10,94 +11,152 @@ import ( "github.com/containers/image/v5/docker/reference" "github.com/containers/image/v5/internal/imagedestination" internalsig "github.com/containers/image/v5/internal/signature" - "github.com/containers/image/v5/manifest" - "github.com/containers/image/v5/signature" + internalSigner "github.com/containers/image/v5/internal/signer" + "github.com/containers/image/v5/signature/signer" "github.com/containers/image/v5/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -const ( - testGPGHomeDirectory = "../signature/fixtures" - // TestKeyFingerprint is the fingerprint of the private key in testGPGHomeDirectory. - // Keep this in sync with signature/fixtures_info_test.go - testKeyFingerprint = "1D8230F6CDB6A06716E414C1DB72F2188BB46CC8" -) +// stubSignerImpl is a signer.SigningImplementation that allows us to check the signed identity, without the overhead of actually signing. +// We abuse internalsig.Sigstore to store the signed manifest and identity in the payload and MIME type fields, respectively. +type stubSignerImpl struct { + signingFailure error // if set, SignImageManifest returns this +} -func TestCreateSignature(t *testing.T) { - manifestBlob := []byte("Something") - manifestDigest, err := manifest.Digest(manifestBlob) - require.NoError(t, err) +func (s *stubSignerImpl) ProgressMessage() string { + return "Signing with stubSigner" +} - mech, _, err := signature.NewEphemeralGPGSigningMechanism([]byte{}) - require.NoError(t, err) - defer mech.Close() - if err := mech.SupportsSigning(); err != nil { - t.Skipf("Signing not supported: %v", err) +func (s *stubSignerImpl) SignImageManifest(ctx context.Context, m []byte, dockerReference reference.Named) (internalsig.Signature, error) { + if s.signingFailure != nil { + return nil, s.signingFailure } + return internalsig.SigstoreFromComponents(dockerReference.String(), m, nil), nil +} + +func (s *stubSignerImpl) Close() error { + return nil +} - t.Setenv("GNUPGHOME", testGPGHomeDirectory) +func TestCreateSignatures(t *testing.T) { + stubSigner := internalSigner.NewSigner(&stubSignerImpl{}) + defer stubSigner.Close() - // Signing a directory: reference, which does not have a DockerReference(), fails. + manifestBlob := []byte("Something") + // Set up dir: and docker: destinations tempDir := t.TempDir() dirRef, err := directory.NewReference(tempDir) require.NoError(t, err) dirDest, err := dirRef.NewImageDestination(context.Background(), nil) require.NoError(t, err) defer dirDest.Close() - c := &copier{ - dest: imagedestination.FromPublic(dirDest), - reportWriter: io.Discard, - } - _, err = c.createSignature(manifestBlob, testKeyFingerprint, "", nil) - assert.Error(t, err) - - // Set up a docker: reference dockerRef, err := docker.ParseReference("//busybox") require.NoError(t, err) dockerDest, err := dockerRef.NewImageDestination(context.Background(), &types.SystemContext{RegistriesDirPath: "/this/does/not/exist", DockerPerHostCertDirPath: "/this/does/not/exist"}) require.NoError(t, err) defer dockerDest.Close() - c = &copier{ - dest: imagedestination.FromPublic(dockerDest), - reportWriter: io.Discard, - } - // Signing with an unknown key fails - _, err = c.createSignature(manifestBlob, "this key does not exist", "", nil) - assert.Error(t, err) + workingOptions := Options{Signers: []*signer.Signer{stubSigner}} + for _, cc := range []struct { + name string + dest types.ImageDestination + options *Options + identity string + successWithNoSigs bool + successfullySignedIdentity string // Set to expect a successful signing with workingOptions + }{ + { + name: "signing fails", + dest: dockerDest, + options: &Options{ + Signers: []*signer.Signer{ + internalSigner.NewSigner(&stubSignerImpl{signingFailure: errors.New("fails")}), + }, + }, + }, + { + name: "second signing fails", + dest: dockerDest, + options: &Options{ + Signers: []*signer.Signer{ + stubSigner, + internalSigner.NewSigner(&stubSignerImpl{signingFailure: errors.New("fails")}), + }, + }, + }, + { + name: "not a full reference", + dest: dockerDest, + identity: "myregistry.io/myrepo", + }, + { + name: "dir: with no identity specified, but no signing request", + dest: dirDest, + options: &Options{}, + successWithNoSigs: true, + }, - // Can't sign without a full reference - ref, err := reference.ParseNamed("myregistry.io/myrepo") - require.NoError(t, err) - _, err = c.createSignature(manifestBlob, testKeyFingerprint, "", ref) - assert.Error(t, err) + { + name: "dir: with no identity specified", + dest: dirDest, + identity: "", + }, + { + name: "dir: with overridden identity", + dest: dirDest, + identity: "myregistry.io/myrepo:mytag", + successfullySignedIdentity: "myregistry.io/myrepo:mytag", + }, + { + name: "docker:// without overriding the identity", + dest: dockerDest, + identity: "", + successfullySignedIdentity: "docker.io/library/busybox:latest", + }, + { + name: "docker:// with overidden identity", + dest: dockerDest, + identity: "myregistry.io/myrepo:mytag", + successfullySignedIdentity: "myregistry.io/myrepo:mytag", + }, + } { + var identity reference.Named = nil + if cc.identity != "" { + i, err := reference.ParseNormalizedNamed(cc.identity) + require.NoError(t, err, cc.name) + identity = i + } + options := cc.options + if options == nil { + options = &workingOptions + } - // Mechanism for verifying the signatures - mech, err = signature.NewGPGSigningMechanism() - require.NoError(t, err) - defer mech.Close() + c := &copier{ + dest: imagedestination.FromPublic(cc.dest), + reportWriter: io.Discard, + } + defer c.close() + err := c.setupSigners(options) + require.NoError(t, err, cc.name) + sigs, err := c.createSignatures(context.Background(), manifestBlob, identity) + switch { + case cc.successfullySignedIdentity != "": + require.NoError(t, err, cc.name) + require.Len(t, sigs, 1, cc.name) + stubSig, ok := sigs[0].(internalsig.Sigstore) + require.True(t, ok, cc.name) + // Compare how stubSignerImpl.SignImageManifest stuffs the signing parameters into these fields. + assert.Equal(t, manifestBlob, stubSig.UntrustedPayload(), cc.name) + assert.Equal(t, cc.successfullySignedIdentity, stubSig.UntrustedMIMEType(), cc.name) - // Signing without overriding the identity uses the docker reference - sig, err := c.createSignature(manifestBlob, testKeyFingerprint, "", nil) - require.NoError(t, err) - simpleSig, ok := sig.(internalsig.SimpleSigning) - require.True(t, ok) - verified, err := signature.VerifyDockerManifestSignature(simpleSig.UntrustedSignature(), manifestBlob, "docker.io/library/busybox:latest", mech, testKeyFingerprint) - require.NoError(t, err) - assert.Equal(t, "docker.io/library/busybox:latest", verified.DockerReference) - assert.Equal(t, manifestDigest, verified.DockerManifestDigest) + case cc.successWithNoSigs: + require.NoError(t, err, cc.name) + require.Empty(t, sigs, cc.name) - // Can override the identity with own - ref, err = reference.ParseNamed("myregistry.io/myrepo:mytag") - require.NoError(t, err) - sig, err = c.createSignature(manifestBlob, testKeyFingerprint, "", ref) - require.NoError(t, err) - simpleSig, ok = sig.(internalsig.SimpleSigning) - require.True(t, ok) - verified, err = signature.VerifyDockerManifestSignature(simpleSig.UntrustedSignature(), manifestBlob, "myregistry.io/myrepo:mytag", mech, testKeyFingerprint) - require.NoError(t, err) - assert.Equal(t, "myregistry.io/myrepo:mytag", verified.DockerReference) - assert.Equal(t, manifestDigest, verified.DockerManifestDigest) + default: + assert.Error(t, err, cc.name) + } + } } diff --git a/directory/directory_dest.go b/directory/directory_dest.go index 47a5c17cd5..55b29fe17a 100644 --- a/directory/directory_dest.go +++ b/directory/directory_dest.go @@ -105,7 +105,7 @@ func newImageDestination(sys *types.SystemContext, ref dirReference) (private.Im AcceptsForeignLayerURLs: false, MustMatchRuntimeOS: false, IgnoresEmbeddedDockerReference: false, // N/A, DockerReference() returns nil. - HasThreadSafePutBlob: false, + HasThreadSafePutBlob: true, }), NoPutBlobPartialInitialize: stubs.NoPutBlobPartial(ref), diff --git a/directory/directory_src.go b/directory/directory_src.go index 98efdedd73..5fc83bb6f9 100644 --- a/directory/directory_src.go +++ b/directory/directory_src.go @@ -8,9 +8,9 @@ import ( "github.com/containers/image/v5/internal/imagesource/impl" "github.com/containers/image/v5/internal/imagesource/stubs" + "github.com/containers/image/v5/internal/manifest" "github.com/containers/image/v5/internal/private" "github.com/containers/image/v5/internal/signature" - "github.com/containers/image/v5/manifest" "github.com/containers/image/v5/types" "github.com/opencontainers/go-digest" ) diff --git a/directory/directory_transport.go b/directory/directory_transport.go index 253ecb2475..7e3068693d 100644 --- a/directory/directory_transport.go +++ b/directory/directory_transport.go @@ -89,7 +89,7 @@ func (ref dirReference) Transport() types.ImageTransport { // StringWithinTransport returns a string representation of the reference, which MUST be such that // reference.Transport().ParseReference(reference.StringWithinTransport()) returns an equivalent reference. // NOTE: The returned string is not promised to be equal to the original input to ParseReference; -// e.g. default attribute values omitted by the user may be filled in in the return value, or vice versa. +// e.g. default attribute values omitted by the user may be filled in the return value, or vice versa. // WARNING: Do not use the return value in the UI to describe an image, it does not contain the Transport().Name() prefix. func (ref dirReference) StringWithinTransport() string { return ref.path diff --git a/directory/explicitfilepath/path.go b/directory/explicitfilepath/path.go index b4ff4d08a5..32ae1ae8a7 100644 --- a/directory/explicitfilepath/path.go +++ b/directory/explicitfilepath/path.go @@ -9,7 +9,7 @@ import ( // ResolvePathToFullyExplicit returns the input path converted to an absolute, no-symlinks, cleaned up path. // To do so, all elements of the input path must exist; as a special case, the final component may be // a non-existent name (but not a symlink pointing to a non-existent name) -// This is intended as a a helper for implementations of types.ImageReference.PolicyConfigurationIdentity etc. +// This is intended as a helper for implementations of types.ImageReference.PolicyConfigurationIdentity etc. func ResolvePathToFullyExplicit(path string) (string, error) { switch _, err := os.Lstat(path); { case err == nil: diff --git a/directory/explicitfilepath/path_test.go b/directory/explicitfilepath/path_test.go index f1eedcaca9..7a4c24deba 100644 --- a/directory/explicitfilepath/path_test.go +++ b/directory/explicitfilepath/path_test.go @@ -133,7 +133,7 @@ func runPathResolvingTestCase(t *testing.T, f func(string) (string, error), c pa input := c.setup(t, topDir) + suffix // Do not call filepath.Join() on input, it calls filepath.Clean() internally! description := fmt.Sprintf("%s vs. %s%s", input, c.expected, suffix) - fullOutput, err := ResolvePathToFullyExplicit(topDir + "/" + input) + fullOutput, err := f(topDir + "/" + input) if c.expected == "" { assert.Error(t, err, description) } else { diff --git a/docker/archive/src.go b/docker/archive/src.go index 5604a51218..c4ab9a8c15 100644 --- a/docker/archive/src.go +++ b/docker/archive/src.go @@ -1,8 +1,6 @@ package archive import ( - "context" - "github.com/containers/image/v5/docker/internal/tarfile" "github.com/containers/image/v5/internal/private" "github.com/containers/image/v5/types" @@ -15,7 +13,7 @@ type archiveImageSource struct { // newImageSource returns a types.ImageSource for the specified image reference. // The caller must call .Close() on the returned ImageSource. -func newImageSource(ctx context.Context, sys *types.SystemContext, ref archiveReference) (private.ImageSource, error) { +func newImageSource(sys *types.SystemContext, ref archiveReference) (private.ImageSource, error) { var archive *tarfile.Reader var closeArchive bool if ref.archiveReader != nil { diff --git a/docker/archive/transport.go b/docker/archive/transport.go index 304a8c618f..39e92cac35 100644 --- a/docker/archive/transport.go +++ b/docker/archive/transport.go @@ -62,24 +62,23 @@ func ParseReference(refString string) (types.ImageReference, error) { return nil, fmt.Errorf("docker-archive reference %s isn't of the form [:]", refString) } - parts := strings.SplitN(refString, ":", 2) - path := parts[0] + path, tagOrIndex, gotTagOrIndex := strings.Cut(refString, ":") var nt reference.NamedTagged sourceIndex := -1 - if len(parts) == 2 { + if gotTagOrIndex { // A :tag or :@index was specified. - if len(parts[1]) > 0 && parts[1][0] == '@' { - i, err := strconv.Atoi(parts[1][1:]) + if len(tagOrIndex) > 0 && tagOrIndex[0] == '@' { + i, err := strconv.Atoi(tagOrIndex[1:]) if err != nil { - return nil, fmt.Errorf("Invalid source index %s: %w", parts[1], err) + return nil, fmt.Errorf("Invalid source index %s: %w", tagOrIndex, err) } if i < 0 { return nil, fmt.Errorf("Invalid source index @%d: must not be negative", i) } sourceIndex = i } else { - ref, err := reference.ParseNormalizedNamed(parts[1]) + ref, err := reference.ParseNormalizedNamed(tagOrIndex) if err != nil { return nil, fmt.Errorf("docker-archive parsing reference: %w", err) } @@ -137,7 +136,7 @@ func (ref archiveReference) Transport() types.ImageTransport { // StringWithinTransport returns a string representation of the reference, which MUST be such that // reference.Transport().ParseReference(reference.StringWithinTransport()) returns an equivalent reference. // NOTE: The returned string is not promised to be equal to the original input to ParseReference; -// e.g. default attribute values omitted by the user may be filled in in the return value, or vice versa. +// e.g. default attribute values omitted by the user may be filled in the return value, or vice versa. // WARNING: Do not use the return value in the UI to describe an image, it does not contain the Transport().Name() prefix. func (ref archiveReference) StringWithinTransport() string { switch { @@ -191,7 +190,7 @@ func (ref archiveReference) NewImage(ctx context.Context, sys *types.SystemConte // NewImageSource returns a types.ImageSource for this reference. // The caller must call .Close() on the returned ImageSource. func (ref archiveReference) NewImageSource(ctx context.Context, sys *types.SystemContext) (types.ImageSource, error) { - return newImageSource(ctx, sys, ref) + return newImageSource(sys, ref) } // NewImageDestination returns a types.ImageDestination for this reference. diff --git a/docker/archive/writer.go b/docker/archive/writer.go index aa8cdd6005..11f797c007 100644 --- a/docker/archive/writer.go +++ b/docker/archive/writer.go @@ -19,7 +19,7 @@ type Writer struct { archive *tarfile.Writer writer io.Closer - // The following state can only be acccessed with the mutex held. + // The following state can only be accessed with the mutex held. mutex sync.Mutex hadCommit bool // At least one successful commit has happened } @@ -64,7 +64,7 @@ func NewWriter(sys *types.SystemContext, path string) (*Writer, error) { }, nil } -// imageCommitted notifies the Writer that at least one image was successfully commited to the stream. +// imageCommitted notifies the Writer that at least one image was successfully committed to the stream. func (w *Writer) imageCommitted() { w.mutex.Lock() defer w.mutex.Unlock() diff --git a/docker/body_reader.go b/docker/body_reader.go new file mode 100644 index 0000000000..7d66ef6bc0 --- /dev/null +++ b/docker/body_reader.go @@ -0,0 +1,253 @@ +package docker + +import ( + "context" + "errors" + "fmt" + "io" + "math" + "math/rand" + "net/http" + "net/url" + "strconv" + "strings" + "syscall" + "time" + + "github.com/sirupsen/logrus" +) + +const ( + // bodyReaderMinimumProgress is the minimum progress we consider a good reason to retry + bodyReaderMinimumProgress = 1 * 1024 * 1024 + // bodyReaderMSSinceLastRetry is the minimum time since a last retry we consider a good reason to retry + bodyReaderMSSinceLastRetry = 60 * 1_000 +) + +// bodyReader is an io.ReadCloser returned by dockerImageSource.GetBlob, +// which can transparently resume some (very limited) kinds of aborted connections. +type bodyReader struct { + ctx context.Context + c *dockerClient + path string // path to pass to makeRequest to retry + logURL *url.URL // a string to use in error messages + firstConnectionTime time.Time + + body io.ReadCloser // The currently open connection we use to read data, or nil if there is nothing to read from / close. + lastRetryOffset int64 // -1 if N/A + lastRetryTime time.Time // time.Time{} if N/A + offset int64 // Current offset within the blob + lastSuccessTime time.Time // time.Time{} if N/A +} + +// newBodyReader creates a bodyReader for request path in c. +// firstBody is an already correctly opened body for the blob, returning the full blob from the start. +// If reading from firstBody fails, bodyReader may heuristically decide to resume. +func newBodyReader(ctx context.Context, c *dockerClient, path string, firstBody io.ReadCloser) (io.ReadCloser, error) { + logURL, err := c.resolveRequestURL(path) + if err != nil { + return nil, err + } + res := &bodyReader{ + ctx: ctx, + c: c, + path: path, + logURL: logURL, + firstConnectionTime: time.Now(), + + body: firstBody, + lastRetryOffset: -1, + lastRetryTime: time.Time{}, + offset: 0, + lastSuccessTime: time.Time{}, + } + return res, nil +} + +// parseDecimalInString ensures that s[start:] starts with a non-negative decimal number, and returns that number and the offset after the number. +func parseDecimalInString(s string, start int) (int64, int, error) { + i := start + for i < len(s) && s[i] >= '0' && s[i] <= '9' { + i++ + } + if i == start { + return -1, -1, errors.New("missing decimal number") + } + v, err := strconv.ParseInt(s[start:i], 10, 64) + if err != nil { + return -1, -1, fmt.Errorf("parsing number: %w", err) + } + return v, i, nil +} + +// parseExpectedChar ensures that s[pos] is the expected byte, and returns the offset after it. +func parseExpectedChar(s string, pos int, expected byte) (int, error) { + if pos == len(s) || s[pos] != expected { + return -1, fmt.Errorf("missing expected %q", expected) + } + return pos + 1, nil +} + +// parseContentRange ensures that res contains a Content-Range header with a byte range, and returns (first, last, completeLength) on success. Size can be -1. +func parseContentRange(res *http.Response) (int64, int64, int64, error) { + hdrs := res.Header.Values("Content-Range") + switch len(hdrs) { + case 0: + return -1, -1, -1, errors.New("missing Content-Range: header") + case 1: + break + default: + return -1, -1, -1, fmt.Errorf("ambiguous Content-Range:, %d header values", len(hdrs)) + } + hdr := hdrs[0] + expectedPrefix := "bytes " + if !strings.HasPrefix(hdr, expectedPrefix) { + return -1, -1, -1, fmt.Errorf("invalid Content-Range: %q, missing prefix %q", hdr, expectedPrefix) + } + first, pos, err := parseDecimalInString(hdr, len(expectedPrefix)) + if err != nil { + return -1, -1, -1, fmt.Errorf("invalid Content-Range: %q, parsing first-pos: %w", hdr, err) + } + pos, err = parseExpectedChar(hdr, pos, '-') + if err != nil { + return -1, -1, -1, fmt.Errorf("invalid Content-Range: %q: %w", hdr, err) + } + last, pos, err := parseDecimalInString(hdr, pos) + if err != nil { + return -1, -1, -1, fmt.Errorf("invalid Content-Range: %q, parsing last-pos: %w", hdr, err) + } + pos, err = parseExpectedChar(hdr, pos, '/') + if err != nil { + return -1, -1, -1, fmt.Errorf("invalid Content-Range: %q: %w", hdr, err) + } + completeLength := int64(-1) + if pos < len(hdr) && hdr[pos] == '*' { + pos++ + } else { + completeLength, pos, err = parseDecimalInString(hdr, pos) + if err != nil { + return -1, -1, -1, fmt.Errorf("invalid Content-Range: %q, parsing complete-length: %w", hdr, err) + } + } + if pos < len(hdr) { + return -1, -1, -1, fmt.Errorf("invalid Content-Range: %q, unexpected trailing content", hdr) + } + return first, last, completeLength, nil +} + +// Read implements io.ReadCloser +func (br *bodyReader) Read(p []byte) (int, error) { + if br.body == nil { + return 0, fmt.Errorf("internal error: bodyReader.Read called on a closed object for %s", br.logURL.Redacted()) + } + n, err := br.body.Read(p) + br.offset += int64(n) + switch { + case err == nil || err == io.EOF: + br.lastSuccessTime = time.Now() + return n, err // Unlike the default: case, don’t log anything. + + case errors.Is(err, io.ErrUnexpectedEOF) || errors.Is(err, syscall.ECONNRESET): + originalErr := err + redactedURL := br.logURL.Redacted() + if err := br.errorIfNotReconnecting(originalErr, redactedURL); err != nil { + return n, err + } + + if err := br.body.Close(); err != nil { + logrus.Debugf("Error closing blob body: %v", err) // … and ignore err otherwise + } + br.body = nil + time.Sleep(1*time.Second + time.Duration(rand.Intn(100_000))*time.Microsecond) // Some jitter so that a failure blip doesn’t cause a deterministic stampede + + headers := map[string][]string{ + "Range": {fmt.Sprintf("bytes=%d-", br.offset)}, + } + res, err := br.c.makeRequest(br.ctx, http.MethodGet, br.path, headers, nil, v2Auth, nil) + if err != nil { + return n, fmt.Errorf("%w (while reconnecting: %v)", originalErr, err) + } + consumedBody := false + defer func() { + if !consumedBody { + res.Body.Close() + } + }() + switch res.StatusCode { + case http.StatusPartialContent: // OK + // A client MUST inspect a 206 response's Content-Type and Content-Range field(s) to determine what parts are enclosed and whether additional requests are needed. + // The recipient of an invalid Content-Range MUST NOT attempt to recombine the received content with a stored representation. + first, last, completeLength, err := parseContentRange(res) + if err != nil { + return n, fmt.Errorf("%w (after reconnecting, invalid Content-Range header: %v)", originalErr, err) + } + // We don’t handle responses that start at an unrequested offset, nor responses that terminate before the end of the full blob. + if first != br.offset || (completeLength != -1 && last+1 != completeLength) { + return n, fmt.Errorf("%w (after reconnecting at offset %d, got unexpected Content-Range %d-%d/%d)", originalErr, br.offset, first, last, completeLength) + } + // Continue below + case http.StatusOK: + return n, fmt.Errorf("%w (after reconnecting, server did not process a Range: header, status %d)", originalErr, http.StatusOK) + default: + err := registryHTTPResponseToError(res) + return n, fmt.Errorf("%w (after reconnecting, fetching blob: %v)", originalErr, err) + } + + logrus.Debugf("Successfully reconnected to %s", redactedURL) + consumedBody = true + br.body = res.Body + br.lastRetryOffset = br.offset + br.lastRetryTime = time.Time{} + return n, nil + + default: + logrus.Debugf("Error reading blob body from %s: %#v", br.logURL.Redacted(), err) + return n, err + } +} + +// millisecondsSinceOptional is like currentTime.Sub(tm).Milliseconds, but it returns a floating-point value. +// If tm is time.Time{}, it returns math.NaN() +func millisecondsSinceOptional(currentTime time.Time, tm time.Time) float64 { + if tm == (time.Time{}) { + return math.NaN() + } + return float64(currentTime.Sub(tm).Nanoseconds()) / 1_000_000.0 +} + +// errorIfNotReconnecting makes a heuristic decision whether we should reconnect after err at redactedURL; if so, it returns nil, +// otherwise it returns an appropriate error to return to the caller (possibly augmented with data about the heuristic) +func (br *bodyReader) errorIfNotReconnecting(originalErr error, redactedURL string) error { + currentTime := time.Now() + msSinceFirstConnection := millisecondsSinceOptional(currentTime, br.firstConnectionTime) + msSinceLastRetry := millisecondsSinceOptional(currentTime, br.lastRetryTime) + msSinceLastSuccess := millisecondsSinceOptional(currentTime, br.lastSuccessTime) + logrus.Debugf("Reading blob body from %s failed (%#v), decision inputs: total %d @%.3f ms, last retry %d @%.3f ms, last progress @%.3f ms", + redactedURL, originalErr, br.offset, msSinceFirstConnection, br.lastRetryOffset, msSinceLastRetry, msSinceLastSuccess) + progress := br.offset - br.lastRetryOffset + if progress >= bodyReaderMinimumProgress { + logrus.Infof("Reading blob body from %s failed (%v), reconnecting after %d bytes…", redactedURL, originalErr, progress) + return nil + } + if br.lastRetryTime == (time.Time{}) { + logrus.Infof("Reading blob body from %s failed (%v), reconnecting (first reconnection)…", redactedURL, originalErr) + return nil + } + if msSinceLastRetry >= bodyReaderMSSinceLastRetry { + logrus.Infof("Reading blob body from %s failed (%v), reconnecting after %.3f ms…", redactedURL, originalErr, msSinceLastRetry) + return nil + } + logrus.Debugf("Not reconnecting to %s: insufficient progress %d / time since last retry %.3f ms", redactedURL, progress, msSinceLastRetry) + return fmt.Errorf("(heuristic tuning data: total %d @%.3f ms, last retry %d @%.3f ms, last progress @ %.3f ms): %w", + br.offset, msSinceFirstConnection, br.lastRetryOffset, msSinceLastRetry, msSinceLastSuccess, originalErr) +} + +// Close implements io.ReadCloser +func (br *bodyReader) Close() error { + if br.body == nil { + return nil + } + err := br.body.Close() + br.body = nil + return err +} diff --git a/docker/body_reader_test.go b/docker/body_reader_test.go new file mode 100644 index 0000000000..0011582b7a --- /dev/null +++ b/docker/body_reader_test.go @@ -0,0 +1,196 @@ +package docker + +import ( + "errors" + "math" + "net/http" + "testing" + "time" + + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestParseDecimalInString(t *testing.T) { + for _, prefix := range []string{"", "text", "0"} { + for _, suffix := range []string{"", "text"} { + for _, c := range []struct { + s string + v int64 + }{ + {"0", 0}, + {"1", 1}, + {"0700", 700}, // not octal + } { + input := prefix + c.s + suffix + res, pos, err := parseDecimalInString(input, len(prefix)) + require.NoError(t, err, input) + assert.Equal(t, c.v, res, input) + assert.Equal(t, len(prefix)+len(c.s), pos, input) + } + for _, c := range []string{ + "-1", + "xA", + "&", + "", + "999999999999999999999999999999999999999999999999999999999999999999", + } { + input := prefix + c + suffix + _, _, err := parseDecimalInString(input, len(prefix)) + assert.Error(t, err, c) + } + } + } +} + +func TestParseExpectedChar(t *testing.T) { + for _, prefix := range []string{"", "text", "0"} { + for _, suffix := range []string{"", "text"} { + input := prefix + "+" + suffix + pos, err := parseExpectedChar(input, len(prefix), '+') + require.NoError(t, err, input) + assert.Equal(t, len(prefix)+1, pos, input) + + _, err = parseExpectedChar(input, len(prefix), '-') + assert.Error(t, err, input) + } + } +} + +func TestParseContentRange(t *testing.T) { + for _, c := range []struct { + in string + first, last, completeLength int64 + }{ + {"bytes 0-0/1", 0, 0, 1}, + {"bytes 010-020/030", 10, 20, 30}, + {"bytes 1000-1010/*", 1000, 1010, -1}, + } { + first, last, completeLength, err := parseContentRange(&http.Response{ + Header: http.Header{ + http.CanonicalHeaderKey("Content-Range"): []string{c.in}, + }, + }) + require.NoError(t, err, c.in) + assert.Equal(t, c.first, first, c.in) + assert.Equal(t, c.last, last, c.in) + assert.Equal(t, c.completeLength, completeLength, c.in) + } + + for _, hdr := range []http.Header{ + nil, + {http.CanonicalHeaderKey("Content-Range"): []string{}}, + {http.CanonicalHeaderKey("Content-Range"): []string{"bytes 1-2/3", "bytes 1-2/3"}}, + } { + _, _, _, err := parseContentRange(&http.Response{ + Header: hdr, + }) + assert.Error(t, err) + } + + for _, c := range []string{ + "", + "notbytes 1-2/3", + "bytes ", + "bytes x-2/3", + "bytes 1*2/3", + "bytes 1", + "bytes 1-", + "bytes 1-x/3", + "bytes 1-2", + "bytes 1-2@3", + "bytes 1-2/", + "bytes 1-2/*a", + "bytes 1-2/3a", + } { + _, _, _, err := parseContentRange(&http.Response{ + Header: http.Header{ + http.CanonicalHeaderKey("Content-Range"): []string{c}, + }, + }) + assert.Error(t, err, c, c) + } +} + +func TestMillisecondsSinceOptional(t *testing.T) { + current := time.Date(2023, 2, 9, 8, 7, 6, 5, time.UTC) + res := millisecondsSinceOptional(current, time.Time{}) + assert.True(t, math.IsNaN(res)) + tm := current.Add(-60 * time.Second) // 60 seconds _before_ current + res = millisecondsSinceOptional(current, tm) + assert.Equal(t, res, 60_000.0) +} + +func TestBodyReaderErrorIfNotReconnecting(t *testing.T) { + // Silence logrus.Info logs in the tested method + prevLevel := logrus.StandardLogger().Level + logrus.StandardLogger().SetLevel(logrus.WarnLevel) + t.Cleanup(func() { + logrus.StandardLogger().SetLevel(prevLevel) + }) + + for _, c := range []struct { + name string + previousRetry bool + currentOffset int64 + currentTime int // milliseconds + expectReconnect bool + }{ + { + name: "A lot of progress, after a long time, second retry", + previousRetry: true, + currentOffset: 2 * bodyReaderMinimumProgress, + currentTime: 2 * bodyReaderMSSinceLastRetry, + expectReconnect: true, + }, + { + name: "A lot of progress, after little time, second retry", + previousRetry: true, + currentOffset: 2 * bodyReaderMinimumProgress, + currentTime: 1, + expectReconnect: true, + }, + { + name: "Little progress, after a long time, second retry", + previousRetry: true, + currentOffset: 1, + currentTime: 2 * bodyReaderMSSinceLastRetry, + expectReconnect: true, + }, + { + name: "Little progress, after little time, second retry", + previousRetry: true, + currentOffset: 1, + currentTime: 1, + expectReconnect: false, + }, + { + name: "Little progress, after little time, first retry", + previousRetry: false, + currentOffset: 1, + currentTime: bodyReaderMSSinceLastRetry / 2, + expectReconnect: true, + }, + } { + tm := time.Now() + br := bodyReader{} + if c.previousRetry { + br.lastRetryOffset = 2 * bodyReaderMinimumProgress + br.offset = br.lastRetryOffset + c.currentOffset + br.firstConnectionTime = tm.Add(-time.Duration(c.currentTime+2*bodyReaderMSSinceLastRetry) * time.Millisecond) + br.lastRetryTime = tm.Add(-time.Duration(c.currentTime) * time.Millisecond) + } else { + br.lastRetryOffset = -1 + br.lastRetryTime = time.Time{} + br.offset = c.currentOffset + br.firstConnectionTime = tm.Add(-time.Duration(c.currentTime) * time.Millisecond) + } + err := br.errorIfNotReconnecting(errors.New("some error for error text only"), "URL for error text only") + if c.expectReconnect { + assert.NoError(t, err, c.name, br) + } else { + assert.Error(t, err, c.name, br) + } + } +} diff --git a/docker/daemon/client.go b/docker/daemon/client.go index 323a02fc09..7d2a98d684 100644 --- a/docker/daemon/client.go +++ b/docker/daemon/client.go @@ -30,13 +30,13 @@ func newDockerClient(sys *types.SystemContext) (*dockerclient.Client, error) { // // Similarly, if we want to communicate over plain HTTP on a TCP socket, we also need to set // TLSClientConfig to nil. This can be achieved by using the form `http://` - url, err := dockerclient.ParseHostURL(host) + serverURL, err := dockerclient.ParseHostURL(host) if err != nil { return nil, err } var httpClient *http.Client - if url.Scheme != "unix" { - if url.Scheme == "http" { + if serverURL.Scheme != "unix" { + if serverURL.Scheme == "http" { httpClient = httpConfig() } else { hc, err := tlsConfig(sys) diff --git a/docker/daemon/daemon_transport.go b/docker/daemon/daemon_transport.go index ad21890843..9eedff7456 100644 --- a/docker/daemon/daemon_transport.go +++ b/docker/daemon/daemon_transport.go @@ -118,7 +118,7 @@ func (ref daemonReference) Transport() types.ImageTransport { // StringWithinTransport returns a string representation of the reference, which MUST be such that // reference.Transport().ParseReference(reference.StringWithinTransport()) returns an equivalent reference. // NOTE: The returned string is not promised to be equal to the original input to ParseReference; -// e.g. default attribute values omitted by the user may be filled in in the return value, or vice versa. +// e.g. default attribute values omitted by the user may be filled in the return value, or vice versa. // WARNING: Do not use the return value in the UI to describe an image, it does not contain the Transport().Name() prefix; // instead, see transports.ImageName(). func (ref daemonReference) StringWithinTransport() string { diff --git a/docker/docker_client.go b/docker/docker_client.go index c7ef38b9da..fa749375fb 100644 --- a/docker/docker_client.go +++ b/docker/docker_client.go @@ -1,6 +1,7 @@ package docker import ( + "bytes" "context" "crypto/tls" "encoding/json" @@ -18,12 +19,12 @@ import ( "github.com/containers/image/v5/docker/reference" "github.com/containers/image/v5/internal/iolimits" + "github.com/containers/image/v5/internal/useragent" "github.com/containers/image/v5/manifest" "github.com/containers/image/v5/pkg/docker/config" "github.com/containers/image/v5/pkg/sysregistriesv2" "github.com/containers/image/v5/pkg/tlsclientconfig" "github.com/containers/image/v5/types" - "github.com/containers/image/v5/version" "github.com/containers/storage/pkg/homedir" "github.com/docker/distribution/registry/api/errcode" v2 "github.com/docker/distribution/registry/api/v2" @@ -67,8 +68,6 @@ var ( {path: etcDir + "/containers/certs.d", absolute: true}, {path: etcDir + "/docker/certs.d", absolute: true}, } - - defaultUserAgent = "containers/" + version.Version + " (github.com/containers/image)" ) // extensionSignature and extensionSignatureList come from github.com/openshift/origin/pkg/dockerregistry/server/signaturedispatcher.go: @@ -283,7 +282,7 @@ func newDockerClient(sys *types.SystemContext, registry, reference string) (*doc } tlsClientConfig.InsecureSkipVerify = skipVerify - userAgent := defaultUserAgent + userAgent := useragent.DefaultUserAgent if sys != nil && sys.DockerRegistryUserAgent != "" { userAgent = sys.DockerRegistryUserAgent } @@ -313,8 +312,14 @@ func CheckAuth(ctx context.Context, sys *types.SystemContext, username, password return err } defer resp.Body.Close() - - return httpResponseToError(resp, "") + if resp.StatusCode != http.StatusOK { + err := registryHTTPResponseToError(resp) + if resp.StatusCode == http.StatusUnauthorized { + err = ErrUnauthorizedForCredentials{Err: err} + } + return err + } + return nil } // SearchResult holds the information of each matching image @@ -411,7 +416,7 @@ func SearchRegistry(ctx context.Context, sys *types.SystemContext, registry, ima } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { - err := httpResponseToError(resp, "") + err := registryHTTPResponseToError(resp) logrus.Errorf("error getting search results from v2 endpoint %q: %v", registry, err) return nil, fmt.Errorf("couldn't search registry %q: %w", registry, err) } @@ -443,8 +448,8 @@ func SearchRegistry(ctx context.Context, sys *types.SystemContext, registry, ima if link == "" { break } - linkURLStr := strings.Trim(strings.Split(link, ";")[0], "<>") - linkURL, err := url.Parse(linkURLStr) + linkURLPart, _, _ := strings.Cut(link, ";") + linkURL, err := url.Parse(strings.Trim(linkURLPart, "<>")) if err != nil { return searchRes, err } @@ -467,12 +472,22 @@ func (c *dockerClient) makeRequest(ctx context.Context, method, path string, hea return nil, err } + requestURL, err := c.resolveRequestURL(path) + if err != nil { + return nil, err + } + return c.makeRequestToResolvedURL(ctx, method, requestURL, headers, stream, -1, auth, extraScope) +} + +// resolveRequestURL turns a path for c.makeRequest into a full URL. +// Most users should call makeRequest directly, this exists basically to make the URL available for debug logs. +func (c *dockerClient) resolveRequestURL(path string) (*url.URL, error) { urlString := fmt.Sprintf("%s://%s%s", c.scheme, c.registry, path) - url, err := url.Parse(urlString) + res, err := url.Parse(urlString) if err != nil { return nil, err } - return c.makeRequestToResolvedURL(ctx, method, url, headers, stream, -1, auth, extraScope) + return res, nil } // Checks if the auth headers in the response contain an indication of a failed @@ -515,9 +530,8 @@ func parseRetryAfter(res *http.Response, fallbackDelay time.Duration) time.Durat return time.Duration(num) * time.Second } // Second, check if we have an HTTP date. - // If the delta between the date and now is positive, use it. - // Otherwise, fall back to using the default exponential back off. if t, err := http.ParseTime(after); err == nil { + // If the delta between the date and now is positive, use it. delta := time.Until(t) if delta > 0 { return delta @@ -525,7 +539,6 @@ func parseRetryAfter(res *http.Response, fallbackDelay time.Duration) time.Durat logrus.Debugf("Retry-After date in the past, ignoring it") return fallbackDelay } - // If the header contents are bogus, fall back to using the default exponential back off. logrus.Debugf("Invalid Retry-After format, ignoring it") return fallbackDelay } @@ -535,11 +548,11 @@ func parseRetryAfter(res *http.Response, fallbackDelay time.Duration) time.Durat // makeRequest should generally be preferred. // In case of an HTTP 429 status code in the response, it may automatically retry a few times. // TODO(runcom): too many arguments here, use a struct -func (c *dockerClient) makeRequestToResolvedURL(ctx context.Context, method string, url *url.URL, headers map[string][]string, stream io.Reader, streamLen int64, auth sendAuth, extraScope *authScope) (*http.Response, error) { +func (c *dockerClient) makeRequestToResolvedURL(ctx context.Context, method string, requestURL *url.URL, headers map[string][]string, stream io.Reader, streamLen int64, auth sendAuth, extraScope *authScope) (*http.Response, error) { delay := backoffInitialDelay attempts := 0 for { - res, err := c.makeRequestToResolvedURLOnce(ctx, method, url, headers, stream, streamLen, auth, extraScope) + res, err := c.makeRequestToResolvedURLOnce(ctx, method, requestURL, headers, stream, streamLen, auth, extraScope) attempts++ // By default we use pre-defined scopes per operation. In @@ -560,7 +573,7 @@ func (c *dockerClient) makeRequestToResolvedURL(ctx context.Context, method stri // Note: This retry ignores extraScope. That’s, strictly speaking, incorrect, but we don’t currently // expect the insufficient_scope errors to happen for those callers. If that changes, we can add support // for more than one extra scope. - res, err = c.makeRequestToResolvedURLOnce(ctx, method, url, headers, stream, streamLen, auth, newScope) + res, err = c.makeRequestToResolvedURLOnce(ctx, method, requestURL, headers, stream, streamLen, auth, newScope) extraScope = newScope } } @@ -576,14 +589,14 @@ func (c *dockerClient) makeRequestToResolvedURL(ctx context.Context, method stri if delay > backoffMaxDelay { delay = backoffMaxDelay } - logrus.Debugf("Too many requests to %s: sleeping for %f seconds before next attempt", url.Redacted(), delay.Seconds()) + logrus.Debugf("Too many requests to %s: sleeping for %f seconds before next attempt", requestURL.Redacted(), delay.Seconds()) select { case <-ctx.Done(): return nil, ctx.Err() case <-time.After(delay): // Nothing } - delay = delay * 2 // exponential back off + delay *= 2 // If the registry does not specify a delay, back off exponentially. } } @@ -591,8 +604,8 @@ func (c *dockerClient) makeRequestToResolvedURL(ctx context.Context, method stri // streamLen, if not -1, specifies the length of the data expected on stream. // makeRequest should generally be preferred. // Note that no exponential back off is performed when receiving an http 429 status code. -func (c *dockerClient) makeRequestToResolvedURLOnce(ctx context.Context, method string, url *url.URL, headers map[string][]string, stream io.Reader, streamLen int64, auth sendAuth, extraScope *authScope) (*http.Response, error) { - req, err := http.NewRequestWithContext(ctx, method, url.String(), stream) +func (c *dockerClient) makeRequestToResolvedURLOnce(ctx context.Context, method string, resolvedURL *url.URL, headers map[string][]string, stream io.Reader, streamLen int64, auth sendAuth, extraScope *authScope) (*http.Response, error) { + req, err := http.NewRequestWithContext(ctx, method, resolvedURL.String(), stream) if err != nil { return nil, err } @@ -611,7 +624,7 @@ func (c *dockerClient) makeRequestToResolvedURLOnce(ctx context.Context, method return nil, err } } - logrus.Debugf("%s %s", method, url.Redacted()) + logrus.Debugf("%s %s", method, resolvedURL.Redacted()) res, err := c.client.Do(req) if err != nil { return nil, err @@ -804,19 +817,19 @@ func (c *dockerClient) detectPropertiesHelper(ctx context.Context) error { c.client = &http.Client{Transport: tr} ping := func(scheme string) error { - url, err := url.Parse(fmt.Sprintf(resolvedPingV2URL, scheme, c.registry)) + pingURL, err := url.Parse(fmt.Sprintf(resolvedPingV2URL, scheme, c.registry)) if err != nil { return err } - resp, err := c.makeRequestToResolvedURL(ctx, http.MethodGet, url, nil, nil, -1, noAuth, nil) + resp, err := c.makeRequestToResolvedURL(ctx, http.MethodGet, pingURL, nil, nil, -1, noAuth, nil) if err != nil { - logrus.Debugf("Ping %s err %s (%#v)", url.Redacted(), err.Error(), err) + logrus.Debugf("Ping %s err %s (%#v)", pingURL.Redacted(), err.Error(), err) return err } defer resp.Body.Close() - logrus.Debugf("Ping %s status %d", url.Redacted(), resp.StatusCode) + logrus.Debugf("Ping %s status %d", pingURL.Redacted(), resp.StatusCode) if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusUnauthorized { - return httpResponseToError(resp, "") + return registryHTTPResponseToError(resp) } c.challenges = parseAuthHeader(resp.Header) c.scheme = scheme @@ -834,17 +847,17 @@ func (c *dockerClient) detectPropertiesHelper(ctx context.Context) error { } // best effort to understand if we're talking to a V1 registry pingV1 := func(scheme string) bool { - url, err := url.Parse(fmt.Sprintf(resolvedPingV1URL, scheme, c.registry)) + pingURL, err := url.Parse(fmt.Sprintf(resolvedPingV1URL, scheme, c.registry)) if err != nil { return false } - resp, err := c.makeRequestToResolvedURL(ctx, http.MethodGet, url, nil, nil, -1, noAuth, nil) + resp, err := c.makeRequestToResolvedURL(ctx, http.MethodGet, pingURL, nil, nil, -1, noAuth, nil) if err != nil { - logrus.Debugf("Ping %s err %s (%#v)", url.Redacted(), err.Error(), err) + logrus.Debugf("Ping %s err %s (%#v)", pingURL.Redacted(), err.Error(), err) return false } defer resp.Body.Close() - logrus.Debugf("Ping %s status %d", url.Redacted(), resp.StatusCode) + logrus.Debugf("Ping %s status %d", pingURL.Redacted(), resp.StatusCode) if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusUnauthorized { return false } @@ -902,14 +915,14 @@ func (c *dockerClient) getExternalBlob(ctx context.Context, urls []string) (io.R return nil, 0, errors.New("internal error: getExternalBlob called with no URLs") } for _, u := range urls { - url, err := url.Parse(u) - if err != nil || (url.Scheme != "http" && url.Scheme != "https") { + blobURL, err := url.Parse(u) + if err != nil || (blobURL.Scheme != "http" && blobURL.Scheme != "https") { continue // unsupported url. skip this url. } // NOTE: we must not authenticate on additional URLs as those // can be abused to leak credentials or tokens. Please // refer to CVE-2020-15157 for more information. - resp, err = c.makeRequestToResolvedURL(ctx, http.MethodGet, url, nil, nil, -1, noAuth, nil) + resp, err = c.makeRequestToResolvedURL(ctx, http.MethodGet, blobURL, nil, nil, -1, noAuth, nil) if err == nil { if resp.StatusCode != http.StatusOK { err = fmt.Errorf("error fetching external blob from %q: %d (%s)", u, resp.StatusCode, http.StatusText(resp.StatusCode)) @@ -956,15 +969,23 @@ func (c *dockerClient) getBlob(ctx context.Context, ref dockerReference, info ty if err != nil { return nil, 0, err } - if err := httpResponseToError(res, "Error fetching blob"); err != nil { + if res.StatusCode != http.StatusOK { + err := registryHTTPResponseToError(res) res.Body.Close() - return nil, 0, err + return nil, 0, fmt.Errorf("fetching blob: %w", err) } cache.RecordKnownLocation(ref.Transport(), bicTransportScope(ref), info.Digest, newBICLocationReference(ref)) - return res.Body, getBlobSize(res), nil + blobSize := getBlobSize(res) + + reconnectingReader, err := newBodyReader(ctx, c, path, res.Body) + if err != nil { + res.Body.Close() + return nil, 0, err + } + return reconnectingReader, blobSize, nil } -// getOCIDescriptorContents returns the contents a blob spcified by descriptor in ref, which must fit within limit. +// getOCIDescriptorContents returns the contents a blob specified by descriptor in ref, which must fit within limit. func (c *dockerClient) getOCIDescriptorContents(ctx context.Context, ref dockerReference, desc imgspecv1.Descriptor, maxSize int, cache types.BlobInfoCache) ([]byte, error) { // Note that this copies all kinds of attachments: attestations, and whatever else is there, // not just signatures. We leave the signature consumers to decide based on the MIME type. @@ -973,7 +994,7 @@ func (c *dockerClient) getOCIDescriptorContents(ctx context.Context, ref dockerR return nil, err } defer reader.Close() - payload, err := iolimits.ReadAtMost(reader, iolimits.MaxSignatureBodySize) + payload, err := iolimits.ReadAtMost(reader, maxSize) if err != nil { return nil, fmt.Errorf("reading blob %s in %s: %w", desc.Digest.String(), ref.ref.Name(), err) } @@ -982,16 +1003,22 @@ func (c *dockerClient) getOCIDescriptorContents(ctx context.Context, ref dockerR // isManifestUnknownError returns true iff err from fetchManifest is a “manifest unknown” error. func isManifestUnknownError(err error) bool { - var errs errcode.Errors - if !errors.As(err, &errs) || len(errs) == 0 { - return false - } - err = errs[0] - ec, ok := err.(errcode.ErrorCoder) - if !ok { - return false - } - return ec.ErrorCode() == v2.ErrorCodeManifestUnknown + // docker/distribution, and as defined in the spec + var ec errcode.ErrorCoder + if errors.As(err, &ec) && ec.ErrorCode() == v2.ErrorCodeManifestUnknown { + return true + } + // registry.redhat.io as of October 2022 + var e errcode.Error + if errors.As(err, &e) && e.ErrorCode() == errcode.ErrorCodeUnknown && e.Message == "Not Found" { + return true + } + // ALSO registry.redhat.io as of October 2022 + var unexpected *unexpectedHTTPResponseError + if errors.As(err, &unexpected) && unexpected.StatusCode == http.StatusNotFound && bytes.Contains(unexpected.Response, []byte("Not found")) { + return true + } + return false } // getSigstoreAttachmentManifest loads and parses the manifest for sigstore attachments for @@ -1037,9 +1064,8 @@ func (c *dockerClient) getExtensionsSignatures(ctx context.Context, ref dockerRe return nil, err } defer res.Body.Close() - if res.StatusCode != http.StatusOK { - return nil, fmt.Errorf("downloading signatures for %s in %s: %w", manifestDigest, ref.ref.Name(), handleErrorResponse(res)) + return nil, fmt.Errorf("downloading signatures for %s in %s: %w", manifestDigest, ref.ref.Name(), registryHTTPResponseToError(res)) } body, err := iolimits.ReadAtMost(res.Body, iolimits.MaxSignatureListBodySize) diff --git a/docker/docker_client_test.go b/docker/docker_client_test.go index a7c085215e..086bc132a6 100644 --- a/docker/docker_client_test.go +++ b/docker/docker_client_test.go @@ -1,6 +1,8 @@ package docker import ( + "bufio" + "bytes" "context" "errors" "fmt" @@ -11,6 +13,7 @@ import ( "testing" "time" + "github.com/containers/image/v5/internal/useragent" "github.com/containers/image/v5/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -174,7 +177,7 @@ func TestUserAgent(t *testing.T) { }{ // Can't both test nil and set DockerInsecureSkipTLSVerify :( // {nil, defaultUA}, - {&types.SystemContext{}, defaultUserAgent}, + {&types.SystemContext{}, useragent.DefaultUserAgent}, {&types.SystemContext{DockerRegistryUserAgent: sentinelUA}, sentinelUA}, } { // For this test against localhost, we don't care. @@ -328,3 +331,60 @@ func TestNeedsNoRetry(t *testing.T) { t.Fatal("Got the need to retry, but none should be required") } } + +func TestIsManifestUnknownError(t *testing.T) { + // Mostly a smoke test; we can add more registries here if they need special handling. + + for _, c := range []struct{ name, response string }{ + { + name: "docker.io when a tag in an _existing repo_ is not found", + response: "HTTP/1.1 404 Not Found\r\n" + + "Connection: close\r\n" + + "Content-Length: 109\r\n" + + "Content-Type: application/json\r\n" + + "Date: Thu, 12 Aug 2021 20:51:32 GMT\r\n" + + "Docker-Distribution-Api-Version: registry/2.0\r\n" + + "Ratelimit-Limit: 100;w=21600\r\n" + + "Ratelimit-Remaining: 100;w=21600\r\n" + + "Strict-Transport-Security: max-age=31536000\r\n" + + "\r\n" + + "{\"errors\":[{\"code\":\"MANIFEST_UNKNOWN\",\"message\":\"manifest unknown\",\"detail\":{\"Tag\":\"this-does-not-exist\"}}]}\n", + }, + { + name: "registry.redhat.io/v2/this-does-not-exist/manifests/latest", + response: "HTTP/1.1 404 Not Found\r\n" + + "Connection: close\r\n" + + "Content-Length: 53\r\n" + + "Cache-Control: max-age=0, no-cache, no-store\r\n" + + "Content-Type: application/json\r\n" + + "Date: Thu, 13 Oct 2022 18:15:15 GMT\r\n" + + "Expires: Thu, 13 Oct 2022 18:15:15 GMT\r\n" + + "Pragma: no-cache\r\n" + + "Server: Apache\r\n" + + "Strict-Transport-Security: max-age=63072000; includeSubdomains; preload\r\n" + + "X-Hostname: crane-tbr06.cran-001.prod.iad2.dc.redhat.com\r\n" + + "\r\n" + + "{\"errors\": [{\"code\": \"404\", \"message\": \"Not Found\"}]}\r\n", + }, + { + name: "registry.redhat.io/v2/rhosp15-rhel8/openstack-cron/manifests/sha256-8df5e60c42668706ac108b59c559b9187fa2de7e4e262e2967e3e9da35d5a8d7.sig", + response: "HTTP/1.1 404 Not Found\r\n" + + "Connection: close\r\n" + + "Content-Length: 10\r\n" + + "Accept-Ranges: bytes\r\n" + + "Date: Thu, 13 Oct 2022 18:13:53 GMT\r\n" + + "Server: AkamaiNetStorage\r\n" + + "X-Docker-Size: -1\r\n" + + "\r\n" + + "Not found\r\n", + }, + } { + resp, err := http.ReadResponse(bufio.NewReader(bytes.NewReader([]byte(c.response))), nil) + require.NoError(t, err, c.name) + defer resp.Body.Close() + err = fmt.Errorf("wrapped: %w", registryHTTPResponseToError(resp)) + + res := isManifestUnknownError(err) + assert.True(t, res, "%#v", err, c.name) + } +} diff --git a/docker/docker_image.go b/docker/docker_image.go index 3e8dbbee13..6e121533ed 100644 --- a/docker/docker_image.go +++ b/docker/docker_image.go @@ -77,8 +77,8 @@ func GetRepositoryTags(ctx context.Context, sys *types.SystemContext, ref types. return nil, err } defer res.Body.Close() - if err := httpResponseToError(res, "fetching tags list"); err != nil { - return nil, err + if res.StatusCode != http.StatusOK { + return nil, fmt.Errorf("fetching tags list: %w", registryHTTPResponseToError(res)) } var tagsHolder struct { @@ -94,8 +94,8 @@ func GetRepositoryTags(ctx context.Context, sys *types.SystemContext, ref types. break } - linkURLStr := strings.Trim(strings.Split(link, ";")[0], "<>") - linkURL, err := url.Parse(linkURLStr) + linkURLPart, _, _ := strings.Cut(link, ";") + linkURL, err := url.Parse(strings.Trim(linkURLPart, "<>")) if err != nil { return tags, err } diff --git a/docker/docker_image_dest.go b/docker/docker_image_dest.go index 44b45c472c..9652683852 100644 --- a/docker/docker_image_dest.go +++ b/docker/docker_image_dest.go @@ -21,6 +21,7 @@ import ( "github.com/containers/image/v5/internal/iolimits" "github.com/containers/image/v5/internal/private" "github.com/containers/image/v5/internal/putblobdigest" + "github.com/containers/image/v5/internal/set" "github.com/containers/image/v5/internal/signature" "github.com/containers/image/v5/internal/streamdigest" "github.com/containers/image/v5/internal/uploadreader" @@ -32,6 +33,8 @@ import ( "github.com/opencontainers/go-digest" imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" "github.com/sirupsen/logrus" + "golang.org/x/exp/maps" + "golang.org/x/exp/slices" ) type dockerImageDestination struct { @@ -244,7 +247,7 @@ func (d *dockerImageDestination) blobExists(ctx context.Context, repo reference. logrus.Debugf("... not present") return false, -1, nil default: - return false, -1, fmt.Errorf("failed to read from destination repository %s: %d (%s)", reference.Path(d.ref.ref), res.StatusCode, http.StatusText(res.StatusCode)) + return false, -1, fmt.Errorf("checking whether a blob %s exists in %s: %w", digest, repo.Name(), registryHTTPResponseToError(res)) } } @@ -407,7 +410,7 @@ func (d *dockerImageDestination) TryReusingBlobWithOptions(ctx context.Context, // If the destination is in principle available, refuses this manifest type (e.g. it does not recognize the schema), // but may accept a different manifest type, the returned error must be an ManifestTypeRejectedError. func (d *dockerImageDestination) PutManifest(ctx context.Context, m []byte, instanceDigest *digest.Digest) error { - refTail := "" + var refTail string if instanceDigest != nil { // If the instanceDigest is provided, then use it as the refTail, because the reference, // whether it includes a tag or a digest, refers to the list as a whole, and not this @@ -487,15 +490,10 @@ func successStatus(status int) bool { return status >= 200 && status <= 399 } -// isManifestInvalidError returns true iff err from client.HandleErrorResponse is a “manifest invalid” error. +// isManifestInvalidError returns true iff err from registryHTTPResponseToError is a “manifest invalid” error. func isManifestInvalidError(err error) bool { - errors, ok := err.(errcode.Errors) - if !ok || len(errors) == 0 { - return false - } - err = errors[0] - ec, ok := err.(errcode.ErrorCoder) - if !ok { + var ec errcode.ErrorCoder + if ok := errors.As(err, &ec); !ok { return false } @@ -585,8 +583,8 @@ func (d *dockerImageDestination) putSignaturesToLookaside(signatures []signature // NOTE: Keep this in sync with docs/signature-protocols.md! for i, signature := range signatures { - url := lookasideStorageURL(d.c.signatureBase, manifestDigest, i) - err := d.putOneSignature(url, signature) + sigURL := lookasideStorageURL(d.c.signatureBase, manifestDigest, i) + err := d.putOneSignature(sigURL, signature) if err != nil { return err } @@ -597,8 +595,8 @@ func (d *dockerImageDestination) putSignaturesToLookaside(signatures []signature // is enough for dockerImageSource to stop looking for other signatures, so that // is sufficient. for i := len(signatures); ; i++ { - url := lookasideStorageURL(d.c.signatureBase, manifestDigest, i) - missing, err := d.c.deleteOneSignature(url) + sigURL := lookasideStorageURL(d.c.signatureBase, manifestDigest, i) + missing, err := d.c.deleteOneSignature(sigURL) if err != nil { return err } @@ -610,13 +608,13 @@ func (d *dockerImageDestination) putSignaturesToLookaside(signatures []signature return nil } -// putOneSignature stores sig to url. +// putOneSignature stores sig to sigURL. // NOTE: Keep this in sync with docs/signature-protocols.md! -func (d *dockerImageDestination) putOneSignature(url *url.URL, sig signature.Signature) error { - switch url.Scheme { +func (d *dockerImageDestination) putOneSignature(sigURL *url.URL, sig signature.Signature) error { + switch sigURL.Scheme { case "file": - logrus.Debugf("Writing to %s", url.Path) - err := os.MkdirAll(filepath.Dir(url.Path), 0755) + logrus.Debugf("Writing to %s", sigURL.Path) + err := os.MkdirAll(filepath.Dir(sigURL.Path), 0755) if err != nil { return err } @@ -624,16 +622,16 @@ func (d *dockerImageDestination) putOneSignature(url *url.URL, sig signature.Sig if err != nil { return err } - err = os.WriteFile(url.Path, blob, 0644) + err = os.WriteFile(sigURL.Path, blob, 0644) if err != nil { return err } return nil case "http", "https": - return fmt.Errorf("Writing directly to a %s lookaside %s is not supported. Configure a lookaside-staging: location", url.Scheme, url.Redacted()) + return fmt.Errorf("Writing directly to a %s lookaside %s is not supported. Configure a lookaside-staging: location", sigURL.Scheme, sigURL.Redacted()) default: - return fmt.Errorf("Unsupported scheme when writing signature to %s", url.Redacted()) + return fmt.Errorf("Unsupported scheme when writing signature to %s", sigURL.Redacted()) } } @@ -644,7 +642,7 @@ func (d *dockerImageDestination) putSignaturesToSigstoreAttachments(ctx context. ociManifest, err := d.c.getSigstoreAttachmentManifest(ctx, d.ref, manifestDigest) if err != nil { - return nil + return err } var ociConfig imgspecv1.Image // Most fields empty by default if ociManifest == nil { @@ -653,6 +651,7 @@ func (d *dockerImageDestination) putSignaturesToSigstoreAttachments(ctx context. Digest: "", // We will fill this in later. Size: 0, }, nil) + ociConfig.RootFS.Type = "layers" } else { logrus.Debugf("Fetching sigstore attachment config %s", ociManifest.Config.Digest.String()) // We don’t benefit from a real BlobInfoCache here because we never try to reuse/mount configs. @@ -715,13 +714,13 @@ func (d *dockerImageDestination) putSignaturesToSigstoreAttachments(ctx context. LayerIndex: nil, }) if err != nil { - return nil + return err } ociManifest.Config = configDesc manifestBlob, err := ociManifest.Serialize() if err != nil { - return nil + return err } logrus.Debugf("Uploading sigstore attachment manifest") return d.uploadManifest(ctx, manifestBlob, sigstoreAttachmentTag(manifestDigest)) @@ -735,24 +734,15 @@ func layerMatchesSigstoreSignature(layer imgspecv1.Descriptor, mimeType string, // But right now we don’t want to deal with corner cases like bad digest formats // or unavailable algorithms; in the worst case we end up with duplicate signature // entries. - layer.Digest.String() != digest.FromBytes(payloadBlob).String() { - return false - } - if len(layer.Annotations) != len(annotations) { + layer.Digest.String() != digest.FromBytes(payloadBlob).String() || + !maps.Equal(layer.Annotations, annotations) { return false } - for k, v1 := range layer.Annotations { - if v2, ok := annotations[k]; !ok || v1 != v2 { - return false - } - } - // All annotations in layer exist in sig, and the number of annotations is the same, so all annotations - // in sig also exist in layer. return true } // putBlobBytesAsOCI uploads a blob with the specified contents, and returns an appropriate -// OCI descriptior. +// OCI descriptor. func (d *dockerImageDestination) putBlobBytesAsOCI(ctx context.Context, contents []byte, mimeType string, options private.PutBlobOptions) (imgspecv1.Descriptor, error) { blobDigest := digest.FromBytes(contents) info, err := d.PutBlobWithOptions(ctx, bytes.NewReader(contents), @@ -771,23 +761,23 @@ func (d *dockerImageDestination) putBlobBytesAsOCI(ctx context.Context, contents }, nil } -// deleteOneSignature deletes a signature from url, if it exists. +// deleteOneSignature deletes a signature from sigURL, if it exists. // If it successfully determines that the signature does not exist, returns (true, nil) // NOTE: Keep this in sync with docs/signature-protocols.md! -func (c *dockerClient) deleteOneSignature(url *url.URL) (missing bool, err error) { - switch url.Scheme { +func (c *dockerClient) deleteOneSignature(sigURL *url.URL) (missing bool, err error) { + switch sigURL.Scheme { case "file": - logrus.Debugf("Deleting %s", url.Path) - err := os.Remove(url.Path) + logrus.Debugf("Deleting %s", sigURL.Path) + err := os.Remove(sigURL.Path) if err != nil && os.IsNotExist(err) { return true, nil } return false, err case "http", "https": - return false, fmt.Errorf("Writing directly to a %s lookaside %s is not supported. Configure a lookaside-staging: location", url.Scheme, url.Redacted()) + return false, fmt.Errorf("Writing directly to a %s lookaside %s is not supported. Configure a lookaside-staging: location", sigURL.Scheme, sigURL.Redacted()) default: - return false, fmt.Errorf("Unsupported scheme when deleting signature from %s", url.Redacted()) + return false, fmt.Errorf("Unsupported scheme when deleting signature from %s", sigURL.Redacted()) } } @@ -807,12 +797,11 @@ func (d *dockerImageDestination) putSignaturesToAPIExtension(ctx context.Context if err != nil { return err } - existingSigNames := map[string]struct{}{} + existingSigNames := set.New[string]() for _, sig := range existingSignatures.Signatures { - existingSigNames[sig.Name] = struct{}{} + existingSigNames.Add(sig.Name) } -sigExists: for _, newSigWithFormat := range signatures { newSigSimple, ok := newSigWithFormat.(signature.SimpleSigning) if !ok { @@ -820,10 +809,10 @@ sigExists: } newSig := newSigSimple.UntrustedSignature() - for _, existingSig := range existingSignatures.Signatures { - if existingSig.Version == extensionSignatureSchemaVersion && existingSig.Type == extensionSignatureTypeAtomic && bytes.Equal(existingSig.Content, newSig) { - continue sigExists - } + if slices.ContainsFunc(existingSignatures.Signatures, func(existingSig extensionSignature) bool { + return existingSig.Version == extensionSignatureSchemaVersion && existingSig.Type == extensionSignatureTypeAtomic && bytes.Equal(existingSig.Content, newSig) + }) { + continue } // The API expect us to invent a new unique name. This is racy, but hopefully good enough. @@ -835,7 +824,7 @@ sigExists: return fmt.Errorf("generating random signature len %d: %w", n, err) } signatureName = fmt.Sprintf("%s@%032x", manifestDigest.String(), randBytes) - if _, ok := existingSigNames[signatureName]; !ok { + if !existingSigNames.Contains(signatureName) { break } } diff --git a/docker/docker_image_dest_test.go b/docker/docker_image_dest_test.go index 4238d561bb..8c196e83ab 100644 --- a/docker/docker_image_dest_test.go +++ b/docker/docker_image_dest_test.go @@ -28,6 +28,7 @@ func TestIsManifestInvalidError(t *testing.T) { "{\"errors\":[{\"code\":\"TAG_INVALID\",\"message\":\"manifest tag did not match URI\"}]}\n" resp, err := http.ReadResponse(bufio.NewReader(bytes.NewReader([]byte(response))), nil) require.NoError(t, err) + defer resp.Body.Close() err = registryHTTPResponseToError(resp) res := isManifestInvalidError(err) diff --git a/docker/docker_image_src.go b/docker/docker_image_src.go index b0e8779710..a115268de3 100644 --- a/docker/docker_image_src.go +++ b/docker/docker_image_src.go @@ -10,7 +10,6 @@ import ( "net/http" "net/url" "os" - "regexp" "strings" "sync" @@ -24,10 +23,15 @@ import ( "github.com/containers/image/v5/pkg/blobinfocache/none" "github.com/containers/image/v5/pkg/sysregistriesv2" "github.com/containers/image/v5/types" + "github.com/containers/storage/pkg/regexp" digest "github.com/opencontainers/go-digest" "github.com/sirupsen/logrus" ) +// maxLookasideSignatures is an arbitrary limit for the total number of signatures we would try to read from a lookaside server, +// even if it were broken or malicious and it continued serving an enormous number of items. +const maxLookasideSignatures = 128 + type dockerImageSource struct { impl.Compat impl.PropertyMethodsInitialize @@ -246,7 +250,7 @@ func splitHTTP200ResponseToPartial(streams chan io.ReadCloser, errs chan error, currentOffset += toSkip } s := signalCloseReader{ - closed: make(chan interface{}), + closed: make(chan struct{}), stream: io.NopCloser(io.LimitReader(body, int64(c.Length))), consumeStream: true, } @@ -288,7 +292,7 @@ func handle206Response(streams chan io.ReadCloser, errs chan error, body io.Read return } s := signalCloseReader{ - closed: make(chan interface{}), + closed: make(chan struct{}), stream: p, } streams <- s @@ -299,7 +303,7 @@ func handle206Response(streams chan io.ReadCloser, errs chan error, body io.Read } } -var multipartByteRangesRe = regexp.MustCompile("multipart/byteranges; boundary=([A-Za-z-0-9:]+)") +var multipartByteRangesRe = regexp.Delayed("multipart/byteranges; boundary=([A-Za-z-0-9:]+)") func parseMediaType(contentType string) (string, map[string]string, error) { mediaType, params, err := mime.ParseMediaType(contentType) @@ -331,7 +335,7 @@ func parseMediaType(contentType string) (string, map[string]string, error) { func (s *dockerImageSource) GetBlobAt(ctx context.Context, info types.BlobInfo, chunks []private.ImageSourceChunk) (chan io.ReadCloser, chan error, error) { headers := make(map[string][]string) - var rangeVals []string + rangeVals := make([]string, 0, len(chunks)) for _, c := range chunks { rangeVals = append(rangeVals, fmt.Sprintf("%d-%d", c.Offset, c.Offset+c.Length-1)) } @@ -372,12 +376,9 @@ func (s *dockerImageSource) GetBlobAt(ctx context.Context, info types.BlobInfo, res.Body.Close() return nil, nil, private.BadPartialRequestError{Status: res.Status} default: - err := httpResponseToError(res, "Error fetching partial blob") - if err == nil { - err = fmt.Errorf("invalid status code returned when fetching blob %d (%s)", res.StatusCode, http.StatusText(res.StatusCode)) - } + err := registryHTTPResponseToError(res) res.Body.Close() - return nil, nil, err + return nil, nil, fmt.Errorf("fetching partial blob: %w", err) } } @@ -451,8 +452,12 @@ func (s *dockerImageSource) getSignaturesFromLookaside(ctx context.Context, inst // NOTE: Keep this in sync with docs/signature-protocols.md! signatures := []signature.Signature{} for i := 0; ; i++ { - url := lookasideStorageURL(s.c.signatureBase, manifestDigest, i) - signature, missing, err := s.getOneSignature(ctx, url) + if i >= maxLookasideSignatures { + return nil, fmt.Errorf("server provided %d signatures, assuming that's unreasonable and a server error", maxLookasideSignatures) + } + + sigURL := lookasideStorageURL(s.c.signatureBase, manifestDigest, i) + signature, missing, err := s.getOneSignature(ctx, sigURL) if err != nil { return nil, err } @@ -464,14 +469,14 @@ func (s *dockerImageSource) getSignaturesFromLookaside(ctx context.Context, inst return signatures, nil } -// getOneSignature downloads one signature from url, and returns (signature, false, nil) +// getOneSignature downloads one signature from sigURL, and returns (signature, false, nil) // If it successfully determines that the signature does not exist, returns (nil, true, nil). // NOTE: Keep this in sync with docs/signature-protocols.md! -func (s *dockerImageSource) getOneSignature(ctx context.Context, url *url.URL) (signature.Signature, bool, error) { - switch url.Scheme { +func (s *dockerImageSource) getOneSignature(ctx context.Context, sigURL *url.URL) (signature.Signature, bool, error) { + switch sigURL.Scheme { case "file": - logrus.Debugf("Reading %s", url.Path) - sigBlob, err := os.ReadFile(url.Path) + logrus.Debugf("Reading %s", sigURL.Path) + sigBlob, err := os.ReadFile(sigURL.Path) if err != nil { if os.IsNotExist(err) { return nil, true, nil @@ -480,13 +485,13 @@ func (s *dockerImageSource) getOneSignature(ctx context.Context, url *url.URL) ( } sig, err := signature.FromBlob(sigBlob) if err != nil { - return nil, false, fmt.Errorf("parsing signature %q: %w", url.Path, err) + return nil, false, fmt.Errorf("parsing signature %q: %w", sigURL.Path, err) } return sig, false, nil case "http", "https": - logrus.Debugf("GET %s", url.Redacted()) - req, err := http.NewRequestWithContext(ctx, http.MethodGet, url.String(), nil) + logrus.Debugf("GET %s", sigURL.Redacted()) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, sigURL.String(), nil) if err != nil { return nil, false, err } @@ -496,22 +501,31 @@ func (s *dockerImageSource) getOneSignature(ctx context.Context, url *url.URL) ( } defer res.Body.Close() if res.StatusCode == http.StatusNotFound { + logrus.Debugf("... got status 404, as expected = end of signatures") return nil, true, nil } else if res.StatusCode != http.StatusOK { - return nil, false, fmt.Errorf("reading signature from %s: status %d (%s)", url.Redacted(), res.StatusCode, http.StatusText(res.StatusCode)) + return nil, false, fmt.Errorf("reading signature from %s: status %d (%s)", sigURL.Redacted(), res.StatusCode, http.StatusText(res.StatusCode)) } + + contentType := res.Header.Get("Content-Type") + if mimeType := simplifyContentType(contentType); mimeType == "text/html" { + logrus.Warnf("Signature %q has Content-Type %q, unexpected for a signature", sigURL.Redacted(), contentType) + // Don’t immediately fail; the lookaside spec does not place any requirements on Content-Type. + // If the content really is HTML, it’s going to fail in signature.FromBlob. + } + sigBlob, err := iolimits.ReadAtMost(res.Body, iolimits.MaxSignatureBodySize) if err != nil { return nil, false, err } sig, err := signature.FromBlob(sigBlob) if err != nil { - return nil, false, fmt.Errorf("parsing signature %s: %w", url.Redacted(), err) + return nil, false, fmt.Errorf("parsing signature %s: %w", sigURL.Redacted(), err) } return sig, false, nil default: - return nil, false, fmt.Errorf("Unsupported scheme when reading signature from %s", url.Redacted()) + return nil, false, fmt.Errorf("Unsupported scheme when reading signature from %s", sigURL.Redacted()) } } @@ -605,16 +619,16 @@ func deleteImage(ctx context.Context, sys *types.SystemContext, ref dockerRefere return err } defer get.Body.Close() - manifestBody, err := iolimits.ReadAtMost(get.Body, iolimits.MaxManifestBodySize) - if err != nil { - return err - } switch get.StatusCode { case http.StatusOK: case http.StatusNotFound: return fmt.Errorf("Unable to delete %v. Image may not exist or is not stored with a v2 Schema in a v2 registry", ref.ref) default: - return fmt.Errorf("Failed to delete %v: %s (%v)", ref.ref, manifestBody, get.Status) + return fmt.Errorf("deleting %v: %w", ref.ref, registryHTTPResponseToError(get)) + } + manifestBody, err := iolimits.ReadAtMost(get.Body, iolimits.MaxManifestBodySize) + if err != nil { + return err } manifestDigest, err := manifest.Digest(manifestBody) @@ -630,18 +644,13 @@ func deleteImage(ctx context.Context, sys *types.SystemContext, ref dockerRefere return err } defer delete.Body.Close() - - body, err := iolimits.ReadAtMost(delete.Body, iolimits.MaxErrorBodySize) - if err != nil { - return err - } if delete.StatusCode != http.StatusAccepted { - return fmt.Errorf("Failed to delete %v: %s (%v)", deletePath, string(body), delete.Status) + return fmt.Errorf("deleting %v: %w", ref.ref, registryHTTPResponseToError(delete)) } for i := 0; ; i++ { - url := lookasideStorageURL(c.signatureBase, manifestDigest, i) - missing, err := c.deleteOneSignature(url) + sigURL := lookasideStorageURL(c.signatureBase, manifestDigest, i) + missing, err := c.deleteOneSignature(sigURL) if err != nil { return err } @@ -757,7 +766,7 @@ func makeBufferedNetworkReader(stream io.ReadCloser, nBuffers, bufferSize uint) } type signalCloseReader struct { - closed chan interface{} + closed chan struct{} stream io.ReadCloser consumeStream bool } diff --git a/docker/docker_transport.go b/docker/docker_transport.go index 0544bb3c93..6ae8491594 100644 --- a/docker/docker_transport.go +++ b/docker/docker_transport.go @@ -92,7 +92,7 @@ func (ref dockerReference) Transport() types.ImageTransport { // StringWithinTransport returns a string representation of the reference, which MUST be such that // reference.Transport().ParseReference(reference.StringWithinTransport()) returns an equivalent reference. // NOTE: The returned string is not promised to be equal to the original input to ParseReference; -// e.g. default attribute values omitted by the user may be filled in in the return value, or vice versa. +// e.g. default attribute values omitted by the user may be filled in the return value, or vice versa. // WARNING: Do not use the return value in the UI to describe an image, it does not contain the Transport().Name() prefix. func (ref dockerReference) StringWithinTransport() string { return "//" + reference.FamiliarString(ref.ref) diff --git a/docker/errors.go b/docker/errors.go index 9c02e27e31..2caa10d7d3 100644 --- a/docker/errors.go +++ b/docker/errors.go @@ -4,6 +4,9 @@ import ( "errors" "fmt" "net/http" + + "github.com/docker/distribution/registry/api/errcode" + "github.com/sirupsen/logrus" ) var ( @@ -33,11 +36,11 @@ func httpResponseToError(res *http.Response, context string) error { case http.StatusTooManyRequests: return ErrTooManyRequests case http.StatusUnauthorized: - err := handleErrorResponse(res) + err := registryHTTPResponseToError(res) return ErrUnauthorizedForCredentials{Err: err} default: if context != "" { - context = context + ": " + context += ": " } return fmt.Errorf("%sinvalid status code from registry %d (%s)", context, res.StatusCode, http.StatusText(res.StatusCode)) } @@ -47,12 +50,47 @@ func httpResponseToError(res *http.Response, context string) error { // registry func registryHTTPResponseToError(res *http.Response) error { err := handleErrorResponse(res) - if e, ok := err.(*unexpectedHTTPResponseError); ok { + // len(errs) == 0 should never be returned by handleErrorResponse; if it does, we don't modify it and let the caller report it as is. + if errs, ok := err.(errcode.Errors); ok && len(errs) > 0 { + // The docker/distribution registry implementation almost never returns + // more than one error in the HTTP body; it seems there is only one + // possible instance, where the second error reports a cleanup failure + // we don't really care about. + // + // The only _common_ case where a multi-element error is returned is + // created by the handleErrorResponse parser when OAuth authorization fails: + // the first element contains errors from a WWW-Authenticate header, the second + // element contains errors from the response body. + // + // In that case the first one is currently _slightly_ more informative (ErrorCodeUnauthorized + // for invalid tokens, ErrorCodeDenied for permission denied with a valid token + // for the first error, vs. ErrorCodeUnauthorized for both cases for the second error.) + // + // Also, docker/docker similarly only logs the other errors and returns the + // first one. + if len(errs) > 1 { + logrus.Debugf("Discarding non-primary errors:") + for _, err := range errs[1:] { + logrus.Debugf(" %s", err.Error()) + } + } + err = errs[0] + } + switch e := err.(type) { + case *unexpectedHTTPResponseError: response := string(e.Response) if len(response) > 50 { response = response[:50] + "..." } - err = fmt.Errorf("StatusCode: %d, %s", e.StatusCode, response) + // %.0w makes e visible to error.Unwrap() without including any text + err = fmt.Errorf("StatusCode: %d, %s%.0w", e.StatusCode, response, e) + case errcode.Error: + // e.Error() is fmt.Sprintf("%s: %s", e.Code.Error(), e.Message, which is usually + // rather redundant. So reword it without using e.Code.Error() if e.Message is the default. + if e.Message == e.Code.Message() { + // %.0w makes e visible to error.Unwrap() without including any text + err = fmt.Errorf("%s%.0w", e.Message, e) + } } return err } diff --git a/docker/errors_test.go b/docker/errors_test.go index 72f7b9c396..4149067fec 100644 --- a/docker/errors_test.go +++ b/docker/errors_test.go @@ -8,6 +8,7 @@ import ( "testing" "github.com/docker/distribution/registry/api/errcode" + v2 "github.com/docker/distribution/registry/api/v2" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -16,12 +17,16 @@ import ( // NOR the error texts are an API commitment subject to API stability expectations; // they can change at any time for any reason. func TestRegistryHTTPResponseToError(t *testing.T) { + var unwrappedUnexpectedHTTPResponseError *unexpectedHTTPResponseError + var unwrappedErrcodeError errcode.Error for _, c := range []struct { name string response string errorString string - errorType interface{} // A value of the same type as the expected error, or nil - unwrappedErrorPtr interface{} // A pointer to a value expected to be reachable using errors.As, or nil + errorType any // A value of the same type as the expected error, or nil + unwrappedErrorPtr any // A pointer to a value expected to be reachable using errors.As, or nil + errorCode *errcode.ErrorCode // A matching ErrorCode, or nil + fn func(t *testing.T, err error) // A more specialized test, or nil }{ { name: "HTTP status out of registry error range", @@ -40,7 +45,7 @@ func TestRegistryHTTPResponseToError(t *testing.T) { "JSON? What JSON?\r\n", errorString: "StatusCode: 400, JSON? What JSON?\r\n", errorType: nil, - unwrappedErrorPtr: nil, + unwrappedErrorPtr: &unwrappedUnexpectedHTTPResponseError, }, { name: "401 body not in expected format", @@ -48,9 +53,10 @@ func TestRegistryHTTPResponseToError(t *testing.T) { "Header1: Value1\r\n" + "\r\n" + "JSON? What JSON?\r\n", - errorString: "unauthorized: authentication required", - errorType: errcode.Error{}, - unwrappedErrorPtr: nil, + errorString: "authentication required", + errorType: nil, + unwrappedErrorPtr: &unwrappedErrcodeError, + errorCode: &errcode.ErrorCodeUnauthorized, }, { // docker.io when an image is not found name: "GET https://registry-1.docker.io/v2/library/this-does-not-exist/manifests/latest", @@ -64,9 +70,10 @@ func TestRegistryHTTPResponseToError(t *testing.T) { "Www-Authenticate: Bearer realm=\"https://auth.docker.io/token\",service=\"registry.docker.io\",scope=\"repository:library/this-does-not-exist:pull\",error=\"insufficient_scope\"\r\n" + "\r\n" + "{\"errors\":[{\"code\":\"UNAUTHORIZED\",\"message\":\"authentication required\",\"detail\":[{\"Type\":\"repository\",\"Class\":\"\",\"Name\":\"library/this-does-not-exist\",\"Action\":\"pull\"}]}]}\n", - errorString: "errors:\ndenied: requested access to the resource is denied\nunauthorized: authentication required\n", - errorType: errcode.Errors{}, - unwrappedErrorPtr: nil, + errorString: "requested access to the resource is denied", + errorType: nil, + unwrappedErrorPtr: &unwrappedErrcodeError, + errorCode: &errcode.ErrorCodeDenied, }, { // docker.io when a tag is not found name: "GET https://registry-1.docker.io/v2/library/busybox/manifests/this-does-not-exist", @@ -81,28 +88,95 @@ func TestRegistryHTTPResponseToError(t *testing.T) { "Strict-Transport-Security: max-age=31536000\r\n" + "\r\n" + "{\"errors\":[{\"code\":\"MANIFEST_UNKNOWN\",\"message\":\"manifest unknown\",\"detail\":{\"Tag\":\"this-does-not-exist\"}}]}\n", - errorString: "manifest unknown: manifest unknown", - errorType: errcode.Errors{}, - unwrappedErrorPtr: nil, + errorString: "manifest unknown", + errorType: nil, + unwrappedErrorPtr: &unwrappedErrcodeError, + errorCode: &v2.ErrorCodeManifestUnknown, }, { // public.ecr.aws does not implement tag list name: "GET https://public.ecr.aws/v2/nginx/nginx/tags/list", response: "HTTP/1.1 404 Not Found\r\n" + "Connection: close\r\n" + - "Content-Length: 19\r\n" + - "Content-Type: text/plain; charset=utf-8\r\n" + - "Date: Thu, 12 Aug 2021 19:54:58 GMT\r\n" + + "Content-Length: 65\r\n" + + "Content-Type: application/json; charset=utf-8\r\n" + + "Date: Tue, 06 Sep 2022 21:19:02 GMT\r\n" + "Docker-Distribution-Api-Version: registry/2.0\r\n" + - "X-Content-Type-Options: nosniff\r\n" + "\r\n" + - "404 page not found\n", - errorString: "StatusCode: 404, 404 page not found\n", + "{\"errors\":[{\"code\":\"NOT_FOUND\",\"message\":\"404 page not found\"}]}\r\n", + errorString: "unknown: 404 page not found", errorType: nil, - unwrappedErrorPtr: nil, + unwrappedErrorPtr: &unwrappedErrcodeError, + errorCode: &errcode.ErrorCodeUnknown, + fn: func(t *testing.T, err error) { + var e errcode.Error + ok := errors.As(err, &e) + require.True(t, ok) + // Note: (skopeo inspect) is checking for this errcode.Error value + assert.Equal(t, errcode.Error{ + Code: errcode.ErrorCodeUnknown, // The NOT_FOUND value is not defined, and turns into Unknown + Message: "404 page not found", + Detail: nil, + }, e) + }, + }, + { // registry.redhat.io is not compliant, variant 1: invalid "code" value + name: "registry.redhat.io/v2/this-does-not-exist/manifests/latest", + response: "HTTP/1.1 404 Not Found\r\n" + + "Connection: close\r\n" + + "Content-Length: 53\r\n" + + "Cache-Control: max-age=0, no-cache, no-store\r\n" + + "Content-Type: application/json\r\n" + + "Date: Thu, 13 Oct 2022 18:15:15 GMT\r\n" + + "Expires: Thu, 13 Oct 2022 18:15:15 GMT\r\n" + + "Pragma: no-cache\r\n" + + "Server: Apache\r\n" + + "Strict-Transport-Security: max-age=63072000; includeSubdomains; preload\r\n" + + "X-Hostname: crane-tbr06.cran-001.prod.iad2.dc.redhat.com\r\n" + + "\r\n" + + "{\"errors\": [{\"code\": \"404\", \"message\": \"Not Found\"}]}\r\n", + errorString: "unknown: Not Found", + errorType: errcode.Error{}, + unwrappedErrorPtr: &unwrappedErrcodeError, + errorCode: &errcode.ErrorCodeUnknown, + fn: func(t *testing.T, err error) { + var e errcode.Error + ok := errors.As(err, &e) + require.True(t, ok) + // isManifestUnknownError is checking for this + assert.Equal(t, errcode.Error{ + Code: errcode.ErrorCodeUnknown, // The 404 value is not defined, and turns into Unknown + Message: "Not Found", + Detail: nil, + }, e) + }, + }, + { // registry.redhat.io is not compliant, variant 2: a completely out-of-protocol response + name: "registry.redhat.io/v2/rhosp15-rhel8/openstack-cron/manifests/sha256-8df5e60c42668706ac108b59c559b9187fa2de7e4e262e2967e3e9da35d5a8d7.sig", + response: "HTTP/1.1 404 Not Found\r\n" + + "Connection: close\r\n" + + "Content-Length: 10\r\n" + + "Accept-Ranges: bytes\r\n" + + "Date: Thu, 13 Oct 2022 18:13:53 GMT\r\n" + + "Server: AkamaiNetStorage\r\n" + + "X-Docker-Size: -1\r\n" + + "\r\n" + + "Not found\r\n", + errorString: "StatusCode: 404, Not found\r", + errorType: nil, + unwrappedErrorPtr: &unwrappedUnexpectedHTTPResponseError, + fn: func(t *testing.T, err error) { + var e *unexpectedHTTPResponseError + ok := errors.As(err, &e) + require.True(t, ok) + // isManifestUnknownError is checking for this + assert.Equal(t, 404, e.StatusCode) + assert.Equal(t, []byte("Not found\r"), e.Response) + }, }, } { res, err := http.ReadResponse(bufio.NewReader(bytes.NewReader([]byte(c.response))), nil) require.NoError(t, err, c.name) + defer res.Body.Close() err = registryHTTPResponseToError(res) assert.Equal(t, c.errorString, err.Error(), c.name) @@ -113,5 +187,14 @@ func TestRegistryHTTPResponseToError(t *testing.T) { found := errors.As(err, c.unwrappedErrorPtr) assert.True(t, found, c.name) } + if c.errorCode != nil { + var ec errcode.ErrorCoder + ok := errors.As(err, &ec) + require.True(t, ok, c.name) + assert.Equal(t, *c.errorCode, ec.ErrorCode(), c.name) + } + if c.fn != nil { + c.fn(t, err) + } } } diff --git a/docker/fixtures/registries.d/internal-example.com.yaml b/docker/fixtures/registries.d/internal-example.com.yaml index 05257eb176..aea2512831 100644 --- a/docker/fixtures/registries.d/internal-example.com.yaml +++ b/docker/fixtures/registries.d/internal-example.com.yaml @@ -12,3 +12,7 @@ docker: lookaside: file:///home/mitr/mydevelopment2 localhost/invalid/url/test: lookaside: ":emptyscheme" + localhost/file/path/test: + lookaside: "/no/scheme/just/a/path" + localhost/relative/path/test: + lookaside: "no/scheme/relative/path" diff --git a/docker/internal/tarfile/reader.go b/docker/internal/tarfile/reader.go index eec7b84e52..3b986f503d 100644 --- a/docker/internal/tarfile/reader.go +++ b/docker/internal/tarfile/reader.go @@ -34,15 +34,19 @@ func NewReaderFromFile(sys *types.SystemContext, path string) (*Reader, error) { } defer file.Close() - // If the file is already not compressed we can just return the file itself + // If the file is seekable and already not compressed we can just return the file itself // as a source. Otherwise we pass the stream to NewReaderFromStream. - stream, isCompressed, err := compression.AutoDecompress(file) - if err != nil { - return nil, fmt.Errorf("detecting compression for file %q: %w", path, err) - } - defer stream.Close() - if !isCompressed { - return newReader(path, false) + var stream io.Reader = file + if _, err := file.Seek(0, io.SeekCurrent); err == nil { // seeking is possible + decompressed, isCompressed, err := compression.AutoDecompress(file) + if err != nil { + return nil, fmt.Errorf("detecting compression for file %q: %w", path, err) + } + defer decompressed.Close() + stream = decompressed + if !isCompressed { + return newReader(path, false) + } } return NewReaderFromStream(sys, stream) } diff --git a/docker/internal/tarfile/writer.go b/docker/internal/tarfile/writer.go index f6ee041c49..b5721fef88 100644 --- a/docker/internal/tarfile/writer.go +++ b/docker/internal/tarfile/writer.go @@ -13,10 +13,12 @@ import ( "time" "github.com/containers/image/v5/docker/reference" + "github.com/containers/image/v5/internal/set" "github.com/containers/image/v5/manifest" "github.com/containers/image/v5/types" "github.com/opencontainers/go-digest" "github.com/sirupsen/logrus" + "golang.org/x/exp/slices" ) // Writer allows creating a (docker save)-formatted tar archive containing one or more images. @@ -29,7 +31,7 @@ type Writer struct { // Other state. blobs map[digest.Digest]types.BlobInfo // list of already-sent blobs repositories map[string]map[string]string - legacyLayers map[string]struct{} // A set of IDs of legacy layers that have been already sent. + legacyLayers *set.Set[string] // A set of IDs of legacy layers that have been already sent. manifest []ManifestItem manifestByConfig map[digest.Digest]int // A map from config digest to an entry index in manifest above. } @@ -42,7 +44,7 @@ func NewWriter(dest io.Writer) *Writer { tar: tar.NewWriter(dest), blobs: make(map[digest.Digest]types.BlobInfo), repositories: map[string]map[string]string{}, - legacyLayers: map[string]struct{}{}, + legacyLayers: set.New[string](), manifestByConfig: map[digest.Digest]int{}, } } @@ -89,7 +91,7 @@ func (w *Writer) recordBlobLocked(info types.BlobInfo) { // ensureSingleLegacyLayerLocked writes legacy VERSION and configuration files for a single layer // The caller must have locked the Writer. func (w *Writer) ensureSingleLegacyLayerLocked(layerID string, layerDigest digest.Digest, configBytes []byte) error { - if _, ok := w.legacyLayers[layerID]; !ok { + if !w.legacyLayers.Contains(layerID) { // Create a symlink for the legacy format, where there is one subdirectory per layer ("image"). // See also the comment in physicalLayerPath. physicalLayerPath := w.physicalLayerPath(layerDigest) @@ -106,7 +108,7 @@ func (w *Writer) ensureSingleLegacyLayerLocked(layerID string, layerDigest diges return fmt.Errorf("writing config json file: %w", err) } - w.legacyLayers[layerID] = struct{}{} + w.legacyLayers.Add(layerID) } return nil } @@ -117,7 +119,7 @@ func (w *Writer) writeLegacyMetadataLocked(layerDescriptors []manifest.Schema2De lastLayerID := "" for i, l := range layerDescriptors { // The legacy format requires a config file per layer - layerConfig := make(map[string]interface{}) + layerConfig := make(map[string]any) // The root layer doesn't have any parent if lastLayerID != "" { @@ -188,14 +190,9 @@ func checkManifestItemsMatch(a, b *ManifestItem) error { if a.Config != b.Config { return fmt.Errorf("Internal error: Trying to reuse ManifestItem values with configs %#v vs. %#v", a.Config, b.Config) } - if len(a.Layers) != len(b.Layers) { + if !slices.Equal(a.Layers, b.Layers) { return fmt.Errorf("Internal error: Trying to reuse ManifestItem values with layers %#v vs. %#v", a.Layers, b.Layers) } - for i := range a.Layers { - if a.Layers[i] != b.Layers[i] { - return fmt.Errorf("Internal error: Trying to reuse ManifestItem values with layers[i] %#v vs. %#v", a.Layers[i], b.Layers[i]) - } - } // Ignore RepoTags, that will be built later. // Ignore Parent and LayerSources, which we don’t set to anything meaningful. return nil @@ -229,9 +226,9 @@ func (w *Writer) ensureManifestItemLocked(layerDescriptors []manifest.Schema2Des item = &w.manifest[i] } - knownRepoTags := map[string]struct{}{} + knownRepoTags := set.New[string]() for _, repoTag := range item.RepoTags { - knownRepoTags[repoTag] = struct{}{} + knownRepoTags.Add(repoTag) } for _, tag := range repoTags { // For github.com/docker/docker consumers, this works just as well as @@ -252,9 +249,9 @@ func (w *Writer) ensureManifestItemLocked(layerDescriptors []manifest.Schema2Des // analysis and explanation. refString := fmt.Sprintf("%s:%s", tag.Name(), tag.Tag()) - if _, ok := knownRepoTags[refString]; !ok { + if !knownRepoTags.Contains(refString) { item.RepoTags = append(item.RepoTags, refString) - knownRepoTags[refString] = struct{}{} + knownRepoTags.Add(refString) } } @@ -337,7 +334,7 @@ func (t *tarFI) ModTime() time.Time { func (t *tarFI) IsDir() bool { return false } -func (t *tarFI) Sys() interface{} { +func (t *tarFI) Sys() any { return nil } @@ -346,7 +343,7 @@ func (t *tarFI) Sys() interface{} { func (w *Writer) sendSymlinkLocked(path string, target string) error { hdr, err := tar.FileInfoHeader(&tarFI{path: path, size: 0, isSymlink: true}, target) if err != nil { - return nil + return err } logrus.Debugf("Sending as tar link %s -> %s", path, target) return w.tar.WriteHeader(hdr) @@ -363,7 +360,7 @@ func (w *Writer) sendBytesLocked(path string, b []byte) error { func (w *Writer) sendFileLocked(path string, expectedSize int64, stream io.Reader) error { hdr, err := tar.FileInfoHeader(&tarFI{path: path, size: expectedSize}, "") if err != nil { - return nil + return err } logrus.Debugf("Sending as tar file %s", path) if err := w.tar.WriteHeader(hdr); err != nil { diff --git a/docker/reference/normalize.go b/docker/reference/normalize.go index 6a86ec64fd..d3f47d210f 100644 --- a/docker/reference/normalize.go +++ b/docker/reference/normalize.go @@ -104,7 +104,7 @@ func splitDockerDomain(name string) (domain, remainder string) { } // familiarizeName returns a shortened version of the name familiar -// to to the Docker UI. Familiar names have the default domain +// to the Docker UI. Familiar names have the default domain // "docker.io" and "library/" repository prefix removed. // For example, "docker.io/library/redis" will have the familiar // name "redis" and "docker.io/dmcgowan/myapp" will be "dmcgowan/myapp". diff --git a/docker/reference/normalize_test.go b/docker/reference/normalize_test.go index 08eda0644f..a21c800e14 100644 --- a/docker/reference/normalize_test.go +++ b/docker/reference/normalize_test.go @@ -69,7 +69,7 @@ func TestValidateRemoteName(t *testing.T) { // Allow multiple hyphens as well. "docker---rules/docker", - //Username doc and image name docker being tested. + // Username doc and image name docker being tested. "doc/docker", // single character names are now allowed. @@ -114,7 +114,7 @@ func TestValidateRemoteName(t *testing.T) { // No repository. "docker/", - //namespace too long + // namespace too long "this_is_not_a_valid_namespace_because_its_length_is_greater_than_255_this_is_not_a_valid_namespace_because_its_length_is_greater_than_255_this_is_not_a_valid_namespace_because_its_length_is_greater_than_255_this_is_not_a_valid_namespace_because_its_length_is_greater_than_255/docker", } for _, repositoryName := range invalidRepositoryNames { @@ -258,7 +258,6 @@ func TestParseRepositoryInfo(t *testing.T) { if expected, actual := tcase.RemoteName, Path(r); expected != actual { t.Fatalf("Invalid remoteName for %q. Expected %q, got %q", r, expected, actual) } - } } } diff --git a/docker/reference/reference.go b/docker/reference/reference.go index b7cd00b0d6..6c5484c068 100644 --- a/docker/reference/reference.go +++ b/docker/reference/reference.go @@ -8,8 +8,8 @@ // domain := domain-component ['.' domain-component]* [':' port-number] // domain-component := /([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])/ // port-number := /[0-9]+/ -// path-component := alpha-numeric [separator alpha-numeric]* -// alpha-numeric := /[a-z0-9]+/ +// path-component := alphanumeric [separator alphanumeric]* +// alphanumeric := /[a-z0-9]+/ // separator := /[_.]|__|[-]*/ // // tag := /[\w][\w.-]{0,127}/ @@ -175,7 +175,7 @@ func splitDomain(name string) (string, string) { // hostname and name string. If no valid hostname is // found, the hostname is empty and the full value // is returned as name -// DEPRECATED: Use Domain or Path +// Deprecated: Use Domain or Path func SplitHostname(named Named) (string, string) { if r, ok := named.(namedRepository); ok { return r.Domain(), r.Path() diff --git a/docker/reference/reference_test.go b/docker/reference/reference_test.go index 7ba9935053..ce1a11ddcb 100644 --- a/docker/reference/reference_test.go +++ b/docker/reference/reference_test.go @@ -103,10 +103,10 @@ func TestReferenceParse(t *testing.T) { }, // FIXME "Uppercase" is incorrectly handled as a domain-name here, therefore passes. // See https://github.com/docker/distribution/pull/1778, and https://github.com/docker/docker/pull/20175 - //{ - // input: "Uppercase/lowercase:tag", - // err: ErrNameContainsUppercase, - //}, + // { + // input: "Uppercase/lowercase:tag", + // err: ErrNameContainsUppercase, + // }, { input: "test:5000/Uppercase/lowercase:tag", err: ErrNameContainsUppercase, @@ -231,7 +231,6 @@ func TestReferenceParse(t *testing.T) { } else if ok { failf("unexpected digested type") } - } } @@ -462,7 +461,6 @@ func TestSerialization(t *testing.T) { if _, ok := fieldInterface.(Reference); ok { failf("field should not implement Reference interface") } - } } diff --git a/docker/reference/regexp.go b/docker/reference/regexp.go index 7860349320..76ba5c2d5c 100644 --- a/docker/reference/regexp.go +++ b/docker/reference/regexp.go @@ -1,143 +1,156 @@ package reference -import "regexp" +import ( + "regexp" + "strings" -var ( - // alphaNumericRegexp defines the alpha numeric atom, typically a + storageRegexp "github.com/containers/storage/pkg/regexp" +) + +const ( + // alphaNumeric defines the alpha numeric atom, typically a // component of names. This only allows lower case characters and digits. - alphaNumericRegexp = match(`[a-z0-9]+`) + alphaNumeric = `[a-z0-9]+` - // separatorRegexp defines the separators allowed to be embedded in name + // separator defines the separators allowed to be embedded in name // components. This allow one period, one or two underscore and multiple - // dashes. - separatorRegexp = match(`(?:[._]|__|[-]*)`) + // dashes. Repeated dashes and underscores are intentionally treated + // differently. In order to support valid hostnames as name components, + // supporting repeated dash was added. Additionally double underscore is + // now allowed as a separator to loosen the restriction for previously + // supported names. + separator = `(?:[._]|__|[-]*)` - // nameComponentRegexp restricts registry path component names to start - // with at least one letter or number, with following parts able to be - // separated by one period, one or two underscore and multiple dashes. - nameComponentRegexp = expression( - alphaNumericRegexp, - optional(repeated(separatorRegexp, alphaNumericRegexp))) - - // domainComponentRegexp restricts the registry domain component of a // repository name to start with a component as defined by DomainRegexp // and followed by an optional port. - domainComponentRegexp = match(`(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])`) + domainComponent = `(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])` + + // The string counterpart for TagRegexp. + tag = `[\w][\w.-]{0,127}` + + // The string counterpart for DigestRegexp. + digestPat = `[A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][[:xdigit:]]{32,}` + + // The string counterpart for IdentifierRegexp. + identifier = `([a-f0-9]{64})` + // The string counterpart for ShortIdentifierRegexp. + shortIdentifier = `([a-f0-9]{6,64})` +) + +var ( + // nameComponent restricts registry path component names to start + // with at least one letter or number, with following parts able to be + // separated by one period, one or two underscore and multiple dashes. + nameComponent = expression( + alphaNumeric, + optional(repeated(separator, alphaNumeric))) + + domain = expression( + domainComponent, + optional(repeated(literal(`.`), domainComponent)), + optional(literal(`:`), `[0-9]+`)) // DomainRegexp defines the structure of potential domain components // that may be part of image names. This is purposely a subset of what is // allowed by DNS to ensure backwards compatibility with Docker image // names. - DomainRegexp = expression( - domainComponentRegexp, - optional(repeated(literal(`.`), domainComponentRegexp)), - optional(literal(`:`), match(`[0-9]+`))) + DomainRegexp = re(domain) // TagRegexp matches valid tag names. From docker/docker:graph/tags.go. - TagRegexp = match(`[\w][\w.-]{0,127}`) + TagRegexp = re(tag) + anchoredTag = anchored(tag) // anchoredTagRegexp matches valid tag names, anchored at the start and // end of the matched string. - anchoredTagRegexp = anchored(TagRegexp) + anchoredTagRegexp = storageRegexp.Delayed(anchoredTag) // DigestRegexp matches valid digests. - DigestRegexp = match(`[A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][[:xdigit:]]{32,}`) + DigestRegexp = re(digestPat) + anchoredDigest = anchored(digestPat) // anchoredDigestRegexp matches valid digests, anchored at the start and // end of the matched string. - anchoredDigestRegexp = anchored(DigestRegexp) + anchoredDigestRegexp = storageRegexp.Delayed(anchoredDigest) + namePat = expression( + optional(domain, literal(`/`)), + nameComponent, + optional(repeated(literal(`/`), nameComponent))) // NameRegexp is the format for the name component of references. The // regexp has capturing groups for the domain and name part omitting // the separating forward slash from either. - NameRegexp = expression( - optional(DomainRegexp, literal(`/`)), - nameComponentRegexp, - optional(repeated(literal(`/`), nameComponentRegexp))) + NameRegexp = re(namePat) + anchoredName = anchored( + optional(capture(domain), literal(`/`)), + capture(nameComponent, + optional(repeated(literal(`/`), nameComponent)))) // anchoredNameRegexp is used to parse a name value, capturing the // domain and trailing components. - anchoredNameRegexp = anchored( - optional(capture(DomainRegexp), literal(`/`)), - capture(nameComponentRegexp, - optional(repeated(literal(`/`), nameComponentRegexp)))) + anchoredNameRegexp = storageRegexp.Delayed(anchoredName) + referencePat = anchored(capture(namePat), + optional(literal(":"), capture(tag)), + optional(literal("@"), capture(digestPat))) // ReferenceRegexp is the full supported format of a reference. The regexp // is anchored and has capturing groups for name, tag, and digest // components. - ReferenceRegexp = anchored(capture(NameRegexp), - optional(literal(":"), capture(TagRegexp)), - optional(literal("@"), capture(DigestRegexp))) + ReferenceRegexp = re(referencePat) // IdentifierRegexp is the format for string identifier used as a // content addressable identifier using sha256. These identifiers // are like digests without the algorithm, since sha256 is used. - IdentifierRegexp = match(`([a-f0-9]{64})`) + IdentifierRegexp = re(identifier) // ShortIdentifierRegexp is the format used to represent a prefix // of an identifier. A prefix may be used to match a sha256 identifier // within a list of trusted identifiers. - ShortIdentifierRegexp = match(`([a-f0-9]{6,64})`) + ShortIdentifierRegexp = re(shortIdentifier) + anchoredIdentifier = anchored(identifier) // anchoredIdentifierRegexp is used to check or match an // identifier value, anchored at start and end of string. - anchoredIdentifierRegexp = anchored(IdentifierRegexp) - - // anchoredShortIdentifierRegexp is used to check if a value - // is a possible identifier prefix, anchored at start and end - // of string. - anchoredShortIdentifierRegexp = anchored(ShortIdentifierRegexp) + anchoredIdentifierRegexp = storageRegexp.Delayed(anchoredIdentifier) ) -// match compiles the string to a regular expression. -var match = regexp.MustCompile +// re compiles the string to a regular expression. +var re = regexp.MustCompile // literal compiles s into a literal regular expression, escaping any regexp // reserved characters. -func literal(s string) *regexp.Regexp { - re := match(regexp.QuoteMeta(s)) - - if _, complete := re.LiteralPrefix(); !complete { - panic("must be a literal") - } - - return re +func literal(s string) string { + return regexp.QuoteMeta(s) } // expression defines a full expression, where each regular expression must // follow the previous. -func expression(res ...*regexp.Regexp) *regexp.Regexp { - var s string - for _, re := range res { - s += re.String() - } - - return match(s) +func expression(res ...string) string { + return strings.Join(res, "") } // optional wraps the expression in a non-capturing group and makes the // production optional. -func optional(res ...*regexp.Regexp) *regexp.Regexp { - return match(group(expression(res...)).String() + `?`) +func optional(res ...string) string { + return group(expression(res...)) + `?` } // repeated wraps the regexp in a non-capturing group to get one or more // matches. -func repeated(res ...*regexp.Regexp) *regexp.Regexp { - return match(group(expression(res...)).String() + `+`) +func repeated(res ...string) string { + return group(expression(res...)) + `+` } // group wraps the regexp in a non-capturing group. -func group(res ...*regexp.Regexp) *regexp.Regexp { - return match(`(?:` + expression(res...).String() + `)`) +func group(res ...string) string { + return `(?:` + expression(res...) + `)` } // capture wraps the expression in a capturing group. -func capture(res ...*regexp.Regexp) *regexp.Regexp { - return match(`(` + expression(res...).String() + `)`) +func capture(res ...string) string { + return `(` + expression(res...) + `)` } // anchored anchors the regular expression by adding start and end delimiters. -func anchored(res ...*regexp.Regexp) *regexp.Regexp { - return match(`^` + expression(res...).String() + `$`) +func anchored(res ...string) string { + return `^` + expression(res...) + `$` } diff --git a/docker/reference/regexp_test.go b/docker/reference/regexp_test.go index 48f721943b..383f543ac1 100644 --- a/docker/reference/regexp_test.go +++ b/docker/reference/regexp_test.go @@ -11,8 +11,12 @@ type regexpMatch struct { match bool subs []string } +type Regex interface { + FindStringSubmatch(s string) []string + NumSubexp() int +} -func checkRegexp(t *testing.T, r *regexp.Regexp, m regexpMatch) { +func checkRegexp(t *testing.T, r Regex, m regexpMatch) { matches := r.FindStringSubmatch(m.input) if m.match && matches != nil { if len(matches) != (r.NumSubexp()+1) || matches[0] != m.input { @@ -125,7 +129,7 @@ func TestDomainRegexp(t *testing.T) { func TestFullNameRegexp(t *testing.T) { if anchoredNameRegexp.NumSubexp() != 2 { t.Fatalf("anchored name regexp should have two submatches: %v, %v != 2", - anchoredNameRegexp, anchoredNameRegexp.NumSubexp()) + anchoredNameRegexp.String(), anchoredNameRegexp.NumSubexp()) } testcases := []regexpMatch{ @@ -413,7 +417,7 @@ func TestFullNameRegexp(t *testing.T) { }, } for i := range testcases { - checkRegexp(t, anchoredNameRegexp, testcases[i]) + checkRegexp(t, &anchoredNameRegexp, testcases[i]) } } @@ -512,45 +516,10 @@ func TestIdentifierRegexp(t *testing.T) { }, } - shortCases := []regexpMatch{ - { - input: "da304e823d8ca2b9d863a3c897baeb852ba21ea9a9f1414736394ae7fcaf9821", - match: true, - }, - { - input: "7EC43B381E5AEFE6E04EFB0B3F0693FF2A4A50652D64AEC573905F2DB5889A1C", - match: false, - }, - { - input: "da304e823d8ca2b9d863a3c897baeb852ba21ea9a9f1414736394ae7fcaf", - match: true, - }, - { - input: "sha256:da304e823d8ca2b9d863a3c897baeb852ba21ea9a9f1414736394ae7fcaf9821", - match: false, - }, - { - input: "da304e823d8ca2b9d863a3c897baeb852ba21ea9a9f1414736394ae7fcaf98218482", - match: false, - }, - { - input: "da304", - match: false, - }, - { - input: "da304e", - match: true, - }, - } - for i := range fullCases { - checkRegexp(t, anchoredIdentifierRegexp, fullCases[i]) + checkRegexp(t, &anchoredIdentifierRegexp, fullCases[i]) if IsFullIdentifier(fullCases[i].input) != fullCases[i].match { t.Errorf("Expected match for %q to be %v", fullCases[i].input, fullCases[i].match) } } - - for i := range shortCases { - checkRegexp(t, anchoredShortIdentifierRegexp, shortCases[i]) - } } diff --git a/docker/registries_d.go b/docker/registries_d.go index 37087dd857..c7b884ab3c 100644 --- a/docker/registries_d.go +++ b/docker/registries_d.go @@ -13,9 +13,9 @@ import ( "github.com/containers/image/v5/internal/rootless" "github.com/containers/image/v5/types" "github.com/containers/storage/pkg/homedir" - "github.com/ghodss/yaml" "github.com/opencontainers/go-digest" "github.com/sirupsen/logrus" + "gopkg.in/yaml.v3" ) // systemRegistriesDirPath is the path to registries.d, used for locating lookaside Docker signature storage. @@ -39,18 +39,18 @@ var defaultDockerDir = "/var/lib/containers/sigstore" // registryConfiguration is one of the files in registriesDirPath configuring lookaside locations, or the result of merging them all. // NOTE: Keep this in sync with docs/registries.d.md! type registryConfiguration struct { - DefaultDocker *registryNamespace `json:"default-docker"` + DefaultDocker *registryNamespace `yaml:"default-docker"` // The key is a namespace, using fully-expanded Docker reference format or parent namespaces (per dockerReference.PolicyConfiguration*), - Docker map[string]registryNamespace `json:"docker"` + Docker map[string]registryNamespace `yaml:"docker"` } // registryNamespace defines lookaside locations for a single namespace. type registryNamespace struct { - Lookaside string `json:"lookaside"` // For reading, and if LookasideStaging is not present, for writing. - LookasideStaging string `json:"lookaside-staging"` // For writing only. - SigStore string `json:"sigstore"` // For compatibility, deprecated in favor of Lookaside. - SigStoreStaging string `json:"sigstore-staging"` // For compatibility, deprecated in favor of LookasideStaging. - UseSigstoreAttachments *bool `json:"use-sigstore-attachments,omitempty"` + Lookaside string `yaml:"lookaside"` // For reading, and if LookasideStaging is not present, for writing. + LookasideStaging string `yaml:"lookaside-staging"` // For writing only. + SigStore string `yaml:"sigstore"` // For compatibility, deprecated in favor of Lookaside. + SigStoreStaging string `yaml:"sigstore-staging"` // For compatibility, deprecated in favor of LookasideStaging. + UseSigstoreAttachments *bool `yaml:"use-sigstore-attachments,omitempty"` } // lookasideStorageBase is an "opaque" type representing a lookaside Docker signature storage. @@ -163,17 +163,17 @@ func loadAndMergeConfig(dirPath string) (*registryConfiguration, error) { // the usage of the BaseURL is defined under docker/distribution registries—separate storage of docs/signature-protocols.md func (config *registryConfiguration) lookasideStorageBaseURL(dr dockerReference, write bool) (*url.URL, error) { topLevel := config.signatureTopLevel(dr, write) - var url *url.URL + var baseURL *url.URL if topLevel != "" { u, err := url.Parse(topLevel) if err != nil { return nil, fmt.Errorf("Invalid signature storage URL %s: %w", topLevel, err) } - url = u + baseURL = u } else { // returns default directory if no lookaside specified in configuration file - url = builtinDefaultLookasideStorageDir(rootless.GetRootlessEUID()) - logrus.Debugf(" No signature storage configuration found for %s, using built-in default %s", dr.PolicyConfigurationIdentity(), url.Redacted()) + baseURL = builtinDefaultLookasideStorageDir(rootless.GetRootlessEUID()) + logrus.Debugf(" No signature storage configuration found for %s, using built-in default %s", dr.PolicyConfigurationIdentity(), baseURL.Redacted()) } // NOTE: Keep this in sync with docs/signature-protocols.md! // FIXME? Restrict to explicitly supported schemes? @@ -181,8 +181,8 @@ func (config *registryConfiguration) lookasideStorageBaseURL(dr dockerReference, if path.Clean(repo) != repo { // Coverage: This should not be reachable because /./ and /../ components are not valid in docker references return nil, fmt.Errorf("Unexpected path elements in Docker reference %s for signature storage", dr.ref.String()) } - url.Path = url.Path + "/" + repo - return url, nil + baseURL.Path = baseURL.Path + "/" + repo + return baseURL, nil } // builtinDefaultLookasideStorageDir returns default signature storage URL as per euid @@ -201,8 +201,8 @@ func (config *registryConfiguration) signatureTopLevel(ref dockerReference, writ identity := ref.PolicyConfigurationIdentity() if ns, ok := config.Docker[identity]; ok { logrus.Debugf(` Lookaside configuration: using "docker" namespace %s`, identity) - if url := ns.signatureTopLevel(write); url != "" { - return url + if ret := ns.signatureTopLevel(write); ret != "" { + return ret } } @@ -210,8 +210,8 @@ func (config *registryConfiguration) signatureTopLevel(ref dockerReference, writ for _, name := range ref.PolicyConfigurationNamespaces() { if ns, ok := config.Docker[name]; ok { logrus.Debugf(` Lookaside configuration: using "docker" namespace %s`, name) - if url := ns.signatureTopLevel(write); url != "" { - return url + if ret := ns.signatureTopLevel(write); ret != "" { + return ret } } } @@ -219,8 +219,8 @@ func (config *registryConfiguration) signatureTopLevel(ref dockerReference, writ // Look for a default location if config.DefaultDocker != nil { logrus.Debugf(` Lookaside configuration: using "default-docker" configuration`) - if url := config.DefaultDocker.signatureTopLevel(write); url != "" { - return url + if ret := config.DefaultDocker.signatureTopLevel(write); ret != "" { + return ret } } return "" @@ -287,7 +287,7 @@ func (ns registryNamespace) signatureTopLevel(write bool) string { // base is not nil from the caller // NOTE: Keep this in sync with docs/signature-protocols.md! func lookasideStorageURL(base lookasideStorageBase, manifestDigest digest.Digest, index int) *url.URL { - url := *base - url.Path = fmt.Sprintf("%s@%s=%s/signature-%d", url.Path, manifestDigest.Algorithm(), manifestDigest.Hex(), index+1) - return &url + sigURL := *base + sigURL.Path = fmt.Sprintf("%s@%s=%s/signature-%d", sigURL.Path, manifestDigest.Algorithm(), manifestDigest.Hex(), index+1) + return &sigURL } diff --git a/docker/registries_d_test.go b/docker/registries_d_test.go index d188376a06..8ff505898b 100644 --- a/docker/registries_d_test.go +++ b/docker/registries_d_test.go @@ -21,31 +21,48 @@ func dockerRefFromString(t *testing.T, s string) dockerReference { } func TestSignatureStorageBaseURL(t *testing.T) { - // Error reading configuration directory (/dev/null is not a directory) - _, err := SignatureStorageBaseURL(&types.SystemContext{RegistriesDirPath: "/dev/null"}, - dockerRefFromString(t, "//busybox"), false) - assert.Error(t, err) - - // No match found - // expect default user storage base emptyDir := t.TempDir() - base, err := SignatureStorageBaseURL(&types.SystemContext{RegistriesDirPath: emptyDir}, - dockerRefFromString(t, "//this/is/not/in/the:configuration"), false) - assert.NoError(t, err) - assert.NotNil(t, base) - assert.Equal(t, "file://"+filepath.Join(os.Getenv("HOME"), defaultUserDockerDir, "//this/is/not/in/the"), base.String()) - - // Invalid URL - _, err = SignatureStorageBaseURL(&types.SystemContext{RegistriesDirPath: "fixtures/registries.d"}, - dockerRefFromString(t, "//localhost/invalid/url/test"), false) - assert.Error(t, err) - - // Success - base, err = SignatureStorageBaseURL(&types.SystemContext{RegistriesDirPath: "fixtures/registries.d"}, - dockerRefFromString(t, "//example.com/my/project"), false) - assert.NoError(t, err) - require.NotNil(t, base) - assert.Equal(t, "https://lookaside.example.com/my/project", base.String()) + for _, c := range []struct { + dir, ref string + expected string // Or "" to expect failure + }{ + { // Error reading configuration directory (/dev/null is not a directory) + "/dev/null", "//busybox", + "", + }, + { // No match found: expect default user storage base + emptyDir, "//this/is/not/in/the:configuration", + "file://" + filepath.Join(os.Getenv("HOME"), defaultUserDockerDir, "//this/is/not/in/the"), + }, + { // Invalid URL + "fixtures/registries.d", "//localhost/invalid/url/test", + "", + }, + // URLs without a scheme: This will be rejected by consumers, so we don't really care about + // the returned value, but it should not crash at the very least. + { // Absolute path + "fixtures/registries.d", "//localhost/file/path/test", + "/no/scheme/just/a/path/file/path/test", + }, + { // Relative path + "fixtures/registries.d", "//localhost/relative/path/test", + "no/scheme/relative/path/relative/path/test", + }, + { // Success + "fixtures/registries.d", "//example.com/my/project", + "https://lookaside.example.com/my/project", + }, + } { + base, err := SignatureStorageBaseURL(&types.SystemContext{RegistriesDirPath: c.dir}, + dockerRefFromString(t, c.ref), false) + if c.expected != "" { + require.NoError(t, err, c.ref) + require.NotNil(t, base, c.ref) + assert.Equal(t, c.expected, base.String(), c.ref) + } else { + assert.Error(t, err, c.ref) + } + } } func TestRegistriesDirPath(t *testing.T) { @@ -192,6 +209,8 @@ func TestLoadAndMergeConfig(t *testing.T) { "localhost": {Lookaside: "file:///home/mitr/mydevelopment1"}, "localhost:8080": {Lookaside: "file:///home/mitr/mydevelopment2"}, "localhost/invalid/url/test": {Lookaside: ":emptyscheme"}, + "localhost/file/path/test": {Lookaside: "/no/scheme/just/a/path"}, + "localhost/relative/path/test": {Lookaside: "no/scheme/relative/path"}, "docker.io/contoso": {Lookaside: "https://lookaside.contoso.com/fordocker"}, "docker.io/centos": {Lookaside: "https://lookaside.centos.org/"}, "docker.io/centos/mybetaproduct": { @@ -297,11 +316,11 @@ func TestLookasideStorageURL(t *testing.T) { {"http://localhost:5555/root", 0, "http://localhost:5555/root@" + mdMapped + "/signature-1"}, {"http://localhost:5555/root", 1, "http://localhost:5555/root@" + mdMapped + "/signature-2"}, } { - url, err := url.Parse(c.base) + baseURL, err := url.Parse(c.base) require.NoError(t, err) expectedURL, err := url.Parse(c.expected) require.NoError(t, err) - res := lookasideStorageURL(url, mdInput, c.index) + res := lookasideStorageURL(baseURL, mdInput, c.index) assert.Equal(t, expectedURL, res, c.expected) } } diff --git a/docker/wwwauthenticate.go b/docker/wwwauthenticate.go index 37ca098a81..6bcb835b9e 100644 --- a/docker/wwwauthenticate.go +++ b/docker/wwwauthenticate.go @@ -149,7 +149,7 @@ func expectTokenOrQuoted(s string) (value string, rest string) { p := make([]byte, len(s)-1) j := copy(p, s[:i]) escape := true - for i = i + 1; i < len(s); i++ { + for i++; i < len(s); i++ { b := s[i] switch { case escape: diff --git a/docs/containers-policy.json.5.md b/docs/containers-policy.json.5.md index e13839b5c5..db40eb9425 100644 --- a/docs/containers-policy.json.5.md +++ b/docs/containers-policy.json.5.md @@ -30,7 +30,9 @@ Policy requirements can be defined for: Usually, a scope can be defined to match a single image, and various prefixes of such a most specific scope define namespaces of matching images. + - A default policy for a single transport, expressed using an empty string as a scope + - A global default policy. If multiple policy requirements match a given image, only the requirements from the most specific match apply, @@ -245,12 +247,37 @@ This requirement requires an image to be signed using a sigstore signature with ```js { "type": "sigstoreSigned", - "keyPath": "/path/to/local/keyring/file", - "keyData": "base64-encoded-keyring-data", + "keyPath": "/path/to/local/public/key/file", + "keyData": "base64-encoded-public-key-data", + "fulcio": { + "caPath": "/path/to/local/CA/file", + "caData": "base64-encoded-CA-data", + "oidcIssuer": "https://expected.OIDC.issuer/", + "subjectEmail", "expected-signing-user@example.com", + }, + "rekorPublicKeyPath": "/path/to/local/public/key/file", + "rekorPublicKeyData": "base64-encoded-public-key-data", "signedIdentity": identity_requirement } ``` -Exactly one of `keyPath` and `keyData` must be present, containing a sigstore public key. Only signatures made by this key is accepted. +Exactly one of `keyPath`, `keyData` and `fulcio` must be present. + +If `keyPath` or `keyData` is present, it contains a sigstore public key. +Only signatures made by this key are accepted. + +If `fulcio` is present, the signature must be based on a Fulcio-issued certificate. +One of `caPath` and `caData` must be specified, containing the public key of the Fulcio instance. +Both `oidcIssuer` and `subjectEmail` are mandatory, +exactly specifying the expected identity provider, +and the identity of the user obtaining the Fulcio certificate. + +At most one of `rekorPublicKeyPath` and `rekorPublicKeyData` can be present; +it is mandatory if `fulcio` is specified. +If a Rekor public key is specified, +the signature must have been uploaded to a Rekor server +and the signature must contain an (offline-verifiable) “signed entry timestamp” +proving the existence of the Rekor log record, +signed by the provided public key. The `signedIdentity` field has the same semantics as in the `signedBy` requirement described above. Note that `cosign`-created signatures only contain a repository, so only `matchRepository` and `exactRepository` can be used to accept them (and that does not protect against substitution of a signed image with an unexpected tag). @@ -286,6 +313,21 @@ selectively allow individual transports and scopes as desired. "keyPath": "/path/to/sigstore-pubkey.pub" } ], + /* A sigstore-signed repository using the community Fulcio+Rekor servers. + + The community servers’ public keys can be obtained from + https://github.com/sigstore/sigstore/tree/main/pkg/tuf/repository/targets . */ + "hostname:5000/myns/sigstore-signed-fulcio-rekor": [ + { + "type": "sigstoreSigned", + "fulcio": { + "caPath": "/path/to/fulcio_v1.crt.pem", + "oidcIssuer": "https://github.com/login/oauth", + "subjectEmail": "test-user@example.com" + }, + "rekorPublicKeyPath": "/path/to/rekor.pub", + } + ], /* A sigstore-signed repository, accepts signatures by /usr/bin/cosign */ "hostname:5000/myns/sigstore-signed-allows-malicious-tag-substitution": [ { @@ -293,8 +335,25 @@ selectively allow individual transports and scopes as desired. "keyPath": "/path/to/sigstore-pubkey.pub", "signedIdentity": {"type": "matchRepository"} } + ], + /* A sigstore-signed repository using the community Fulcio+Rekor servers, + accepts signatures by /usr/bin/cosign. + + The community servers’ public keys can be obtained from + https://github.com/sigstore/sigstore/tree/main/pkg/tuf/repository/targets . */ + "hostname:5000/myns/sigstore-signed-fulcio-rekor- allows-malicious-tag-substitution": [ + { + "type": "sigstoreSigned", + "fulcio": { + "caPath": "/path/to/fulcio_v1.crt.pem", + "oidcIssuer": "https://github.com/login/oauth", + "subjectEmail": "test-user@example.com" + }, + "rekorPublicKeyPath": "/path/to/rekor.pub", + "signedIdentity": { "type": "matchRepository" } + } ] - /* Other docker: images use the global default policy and are rejected */ + /* Other docker: images use the global default policy and are rejected */ }, "dir": { "": [{"type": "insecureAcceptAnything"}] /* Allow any images originating in local directories */ diff --git a/docs/containers-signature.5.md b/docs/containers-signature.5.md index 2bbb500c52..cc6f9c3662 100644 --- a/docs/containers-signature.5.md +++ b/docs/containers-signature.5.md @@ -68,7 +68,9 @@ the consumer MUST verify at least the following aspects of the signature (like the `github.com/containers/image/signature` package does): - The blob MUST be a “Signed Message” as defined RFC 4880 section 11.3. - (e.g. it MUST NOT be an unsigned “Literal Message”, or any other non-signature format). + (e.g. it MUST NOT be an unsigned “Literal Message”, + a “Cleartext Signature” as defined in RFC 4880 section 7, + or any other non-signature format). - The signature MUST have been made by an expected key trusted for the purpose (and the specific container image). - The signature MUST be correctly formed and pass the cryptographic validation. - The signature MUST correctly authenticate the included JSON payload @@ -210,7 +212,8 @@ Consumers still SHOULD reject any signature where a member of an `optional` obje ### `optional.creator` -If present, this MUST be a JSON string, identifying the name and version of the software which has created the signature. +If present, this MUST be a JSON string, identifying the name and version of the software which has created the signature +(identifying the low-level software implementation; not the top-level caller). The contents of this string is not defined in detail; however each implementation creating container signatures: diff --git a/docs/containers-sigstore-signing-params.yaml.5.md b/docs/containers-sigstore-signing-params.yaml.5.md new file mode 100644 index 0000000000..f081cdb87d --- /dev/null +++ b/docs/containers-sigstore-signing-params.yaml.5.md @@ -0,0 +1,117 @@ +% CONTAINERS-SIGSTORE-SIGNING-PARAMS.YAML 5 sigstore signing parameters Man Page +% Miloslav Trmač +% January 2023 + +# NAME +containers-sigstore-signing-params.yaml - syntax for the sigstore signing parameter file + +# DESCRIPTION + +Sigstore signing parameter files are used to store options that may be required to create sigstore signatures. +There is no default location for these files; they are user-managed, and used as inputs to a container image signing operation, +e.g. `skopeo copy --sign-by-sigstore=`_param-file_`.yaml` or `podman push --sign-by-sigstore=`_param-file_`.yaml` . + +## FORMAT + +Sigstore signing parameter files use YAML. + +Many parameters are optional, but the file must specify enough to create a signature; +in particular either a private key, or Fulcio. + +### Signing with Private Keys + +- `privateKeyFile:` _path_ + + Create a signature using a private key at _path_. + Existence of this field triggers the use of a private key. + +- `privateKeyPassphraseFile:` _passphrasePath_ + + Read the passphrase required to use `privateKeyFile` from _passphrasePath_. + Optional: if this is not set, the user must provide the passphrase interactively. + +### Signing with Fulcio-generated Certificates + +Instead of a static private key, the signing process generates a short-lived key pair +and requests a Fulcio server to issue a certificate for that key pair, +based on the user authenticating to an OpenID Connect provider. + +To specify Fulcio, include a `fulcio` sub-object with one or more of the following keys. +In addition, a Rekor server must be specified as well. + +- `fulcioURL:` _URL_ + + Required. URL of the Fulcio server to use. + +- `oidcMode:` `interactive` | `deviceGrant` | `staticToken` + + Required. Specifies how to obtain the necessary OpenID Connect credential. + + `interactive` opens a web browser on the same machine, or if that is not possible, + asks the user to open a browser manually and to type in the provided code. + It requires the user to be able to directly interact with the signing process. + + `deviceGrant` uses a device authorization grant flow (RFC 8628). + It requires the user to be able to read text printed by the signing process, and to act on it reasonably promptly. + + `staticToken` provides a pre-existing OpenID Connect “ID token”, which must have been obtained separately. + +- `oidcIssuerURL:` _URL_ + + Required for `oidcMode:` `interactive` or `deviceGrant`. URL of an OpenID Connect issuer server to authenticate with. + +- `oidcClientID:` _client ID_ + + Used for `oidcMode:` `interactive` or `deviceGrant` to identify the client when contacting the issuer. + Optional but likely to be necessary in those cases. + +- `oidcClientSecret:` _client secret_ + + Used for `oidcMode:` `interactive` or `deviceGrant` to authenticate the client when contacting the issuer. + Optional. + +- `oidcIDToken:` _token_ + + Required for `oidcMode: staticToken`. + An OpenID Connect ID token that identifies the user (and authorizes certificate issuance). + +### Recording the Signature to a Rekor Transparency Server + +This can be combined with either a private key or Fulcio. +It is, practically speaking, required for Fulcio; it is optional when a static private key is used, but necessary for +interoperability with the default configuration of `cosign`. + +- `rekorURL`: _URL_ + + URL of the Rekor server to use. + +# EXAMPLES + +### Sign Using a Pre-existing Private Key + +Uses the ”community infrastructure” Rekor server. + +```yaml +privateKeyFile: "/home/user/sigstore/private-key.key" +privateKeyPassphraseFile: "/mnt/user/sigstore-private-key" +rekorURL: "https://rekor.sigstore.dev" +``` + +### Sign Using a Fulcio-Issued Certificate + +Uses the ”community infrastructure” Fulcio and Rekor server, +and the Dex OIDC issuer which delegates to other major issuers like Google and GitHub. + +Other configurations will very likely need to also provide an OIDC client secret. + +```yaml +fulcio: + fulcioURL: "https://fulcio.sigstore.dev" + oidcMode: "interactive" + oidcIssuerURL: "https://oauth2.sigstore.dev/auth" + oidcClientID: "sigstore" +rekorURL: "https://rekor.sigstore.dev" +``` + +# SEE ALSO + skopeo(1), podman(1) diff --git a/go.mod b/go.mod index 6d078ba922..d5be0ac9b7 100644 --- a/go.mod +++ b/go.mod @@ -1,94 +1,129 @@ module github.com/containers/image/v5 -go 1.17 +go 1.18 require ( - github.com/BurntSushi/toml v1.2.0 - github.com/containers/libtrust v0.0.0-20200511145503-9c3a6c22cd9a - github.com/containers/ocicrypt v1.1.5 - github.com/containers/storage v1.42.0 + github.com/BurntSushi/toml v1.2.1 + github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01 + github.com/containers/ocicrypt v1.1.7 + github.com/containers/storage v1.45.3 + github.com/cyberphone/json-canonicalization v0.0.0-20220623050100-57a0ce2678a7 github.com/docker/distribution v2.8.1+incompatible - github.com/docker/docker v20.10.18+incompatible - github.com/docker/docker-credential-helpers v0.6.4 + github.com/docker/docker v23.0.1+incompatible + github.com/docker/docker-credential-helpers v0.7.0 github.com/docker/go-connections v0.4.0 - github.com/ghodss/yaml v1.0.0 + github.com/go-openapi/strfmt v0.21.3 + github.com/go-openapi/swag v0.22.3 github.com/hashicorp/go-multierror v1.1.1 + github.com/hashicorp/go-retryablehttp v0.7.2 github.com/imdario/mergo v0.3.13 - github.com/klauspost/compress v1.15.10 - github.com/klauspost/pgzip v1.2.5 + github.com/klauspost/compress v1.15.15 + github.com/klauspost/pgzip v1.2.6-0.20220930104621-17e8dac29df8 github.com/manifoldco/promptui v0.9.0 github.com/opencontainers/go-digest v1.0.0 - github.com/opencontainers/image-spec v1.0.3-0.20220114050600-8b9d41f48198 - github.com/opencontainers/selinux v1.10.1 + github.com/opencontainers/image-spec v1.1.0-rc2 + github.com/opencontainers/selinux v1.11.0 github.com/ostreedev/ostree-go v0.0.0-20210805093236-719684c64e4f github.com/proglottis/gpgme v0.1.3 - github.com/sigstore/sigstore v1.4.1 + github.com/sigstore/fulcio v1.1.0 + github.com/sigstore/rekor v1.0.1 + github.com/sigstore/sigstore v1.5.1 github.com/sirupsen/logrus v1.9.0 - github.com/stretchr/testify v1.8.0 - github.com/sylabs/sif/v2 v2.7.2 - github.com/theupdateframework/go-tuf v0.5.0 - github.com/ulikunitz/xz v0.5.10 + github.com/stretchr/testify v1.8.1 + github.com/sylabs/sif/v2 v2.9.1 + github.com/theupdateframework/go-tuf v0.5.2 + github.com/ulikunitz/xz v0.5.11 github.com/vbatts/tar-split v0.11.2 - github.com/vbauerster/mpb/v7 v7.5.3 + github.com/vbauerster/mpb/v8 v8.1.6 github.com/xeipuuv/gojsonschema v1.2.0 - go.etcd.io/bbolt v1.3.6 - golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 - golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b - golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 - golang.org/x/term v0.0.0-20220526004731-065cf7ba2467 + go.etcd.io/bbolt v1.3.7 + golang.org/x/crypto v0.6.0 + golang.org/x/exp v0.0.0-20230206171751-46f607a40771 + golang.org/x/oauth2 v0.5.0 + golang.org/x/sync v0.1.0 + golang.org/x/term v0.5.0 + gopkg.in/yaml.v3 v3.0.1 ) require ( github.com/14rcole/gopopulate v0.0.0-20180821133914-b175b219e774 // indirect - github.com/Microsoft/go-winio v0.5.2 // indirect - github.com/Microsoft/hcsshim v0.9.3 // indirect + github.com/Microsoft/go-winio v0.6.0 // indirect + github.com/Microsoft/hcsshim v0.9.6 // indirect github.com/VividCortex/ewma v1.2.0 // indirect github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect + github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d // indirect github.com/chzyer/readline v1.5.1 // indirect github.com/containerd/cgroups v1.0.4 // indirect - github.com/containerd/stargz-snapshotter/estargz v0.12.0 // indirect + github.com/containerd/stargz-snapshotter/estargz v0.13.0 // indirect + github.com/coreos/go-oidc/v3 v3.5.0 // indirect github.com/cyphar/filepath-securejoin v0.2.3 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/docker/go-units v0.4.0 // indirect - github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/go-jose/go-jose/v3 v3.0.0 // indirect + github.com/go-openapi/analysis v0.21.4 // indirect + github.com/go-openapi/errors v0.20.3 // indirect + github.com/go-openapi/jsonpointer v0.19.5 // indirect + github.com/go-openapi/jsonreference v0.20.0 // indirect + github.com/go-openapi/loads v0.21.2 // indirect + github.com/go-openapi/runtime v0.24.1 // indirect + github.com/go-openapi/spec v0.20.7 // indirect + github.com/go-openapi/validate v0.22.0 // indirect + github.com/go-playground/locales v0.14.0 // indirect + github.com/go-playground/universal-translator v0.18.0 // indirect + github.com/go-playground/validator/v10 v10.11.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.2 // indirect - github.com/google/go-containerregistry v0.11.0 // indirect + github.com/google/go-containerregistry v0.12.1 // indirect github.com/google/go-intervals v0.0.2 // indirect + github.com/google/trillian v1.5.1-0.20220819043421-0a389c4bb8d9 // indirect github.com/google/uuid v1.3.0 // indirect github.com/gorilla/mux v1.8.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect - github.com/honeycombio/beeline-go v1.9.0 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/letsencrypt/boulder v0.0.0-20220723181115-27de4befb95e // indirect - github.com/mattn/go-runewidth v0.0.13 // indirect + github.com/leodido/go-urn v1.2.1 // indirect + github.com/letsencrypt/boulder v0.0.0-20230213213521-fdfea0d469b6 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/mattn/go-runewidth v0.0.14 // indirect github.com/mattn/go-shellwords v1.0.12 // indirect github.com/miekg/pkcs11 v1.1.1 // indirect - github.com/mistifyio/go-zfs v2.1.2-0.20190413222219-f784269be439+incompatible // indirect + github.com/mistifyio/go-zfs/v3 v3.0.0 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/moby/sys/mountinfo v0.6.2 // indirect + github.com/moby/term v0.0.0-20221120202655-abb19827d345 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/opencontainers/runc v1.1.3 // indirect + github.com/oklog/ulid v1.3.1 // indirect + github.com/opencontainers/runc v1.1.4 // indirect github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417 // indirect + github.com/opentracing/opentracing-go v1.2.0 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/prometheus/common v0.37.0 // indirect - github.com/rivo/uniseg v0.2.0 // indirect + github.com/rivo/uniseg v0.4.3 // indirect + github.com/segmentio/ksuid v1.0.4 // indirect + github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 // indirect github.com/stefanberger/go-pkcs11uri v0.0.0-20201008174630-78d3cae3a980 // indirect github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 // indirect github.com/tchap/go-patricia v2.3.0+incompatible // indirect github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect + go.mongodb.org/mongo-driver v1.11.1 // indirect go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 // indirect - go.opencensus.io v0.23.0 // indirect - golang.org/x/sys v0.0.0-20220909162455-aba9fc2a8ff2 // indirect - golang.org/x/text v0.3.7 // indirect - google.golang.org/genproto v0.0.0-20220720214146-176da50484ac // indirect - google.golang.org/grpc v1.48.0 // indirect + go.opencensus.io v0.24.0 // indirect + golang.org/x/mod v0.7.0 // indirect + golang.org/x/net v0.6.0 // indirect + golang.org/x/sys v0.5.0 // indirect + golang.org/x/text v0.7.0 // indirect + golang.org/x/tools v0.4.0 // indirect + google.golang.org/appengine v1.6.7 // indirect + google.golang.org/genproto v0.0.0-20230209215440-0dfe4f8abfcc // indirect + google.golang.org/grpc v1.53.0 // indirect google.golang.org/protobuf v1.28.1 // indirect + gopkg.in/go-jose/go-jose.v2 v2.6.1 // indirect gopkg.in/square/go-jose.v2 v2.6.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect + gotest.tools/v3 v3.4.0 // indirect ) diff --git a/go.sum b/go.sum index d0fe0b899f..773d02f248 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,4 @@ -4d63.com/gochecknoglobals v0.1.0/go.mod h1:wfdC5ZjKSPr7CybKEcgJhUOgeAQW1+7WcyK8OvUilfo= bazil.org/fuse v0.0.0-20160811212531-371fbbdaa898/go.mod h1:Xbm+BRKSBEpa4q4hTSxohYNQpsxXPbPry4JJWOB3LB8= -bitbucket.org/creachadair/shell v0.0.6/go.mod h1:8Qqi/cYk7vPnsOePHroKXDJYmb5x7ENhtiFtfZq8K+M= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= @@ -12,110 +10,39 @@ cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6T cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= -cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= -cloud.google.com/go v0.60.0/go.mod h1:yw2G51M9IfRboUH61Us8GqCeF1PzPblB823Mn2q2eAU= -cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= -cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= -cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= -cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= -cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= -cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= -cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= -cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY= -cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM= -cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY= -cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ= -cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= -cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4= -cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= -cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= -cloud.google.com/go v0.100.1/go.mod h1:fs4QogzfH5n2pBXBP9vRiU+eCny7lD2vmFZy79Iuw1U= -cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A= -cloud.google.com/go v0.102.0/go.mod h1:oWcCzKlqJ5zgHQt9YsaeTY9KzIvjyy0ArmiBUgpQ+nc= -cloud.google.com/go v0.102.1/go.mod h1:XZ77E9qnTEnrgEOvr4xzfdX5TRo7fB4T2F4O6+34hIU= -cloud.google.com/go v0.103.0/go.mod h1:vwLx1nqLrzLX/fpwSMOXmFIqBOyHsvHbnAdbGSJ+mKk= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= -cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= -cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= -cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow= -cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM= -cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M= -cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s= -cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU= -cloud.google.com/go/compute v1.7.0/go.mod h1:435lt8av5oL9P3fv1OEzSbSUe+ybHXGMPQHHZWZxy9U= +cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= -cloud.google.com/go/firestore v1.6.0/go.mod h1:afJwI0vaXwAG54kI7A//lP/lSPDkQORQuMkv56TxEPU= -cloud.google.com/go/iam v0.1.0/go.mod h1:vcUNEa0pEm0qRVpmWepWaFMIAI8/hjB9mO8rNCJtF6c= -cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY= -cloud.google.com/go/kms v1.4.0/go.mod h1:fajBHndQ+6ubNw6Ss2sSd+SWvjL26RNo/dr7uxsnnOA= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= -cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= -cloud.google.com/go/pubsub v1.5.0/go.mod h1:ZEwJccE3z93Z2HWvstpri00jOg7oO4UZDtKhwDwqF0w= -cloud.google.com/go/spanner v1.7.0/go.mod h1:sd3K2gZ9Fd0vMPLXzeCrF6fq4i63Q7aTLW/lBIfBkIk= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= -cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= -cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= -cloud.google.com/go/storage v1.22.1/go.mod h1:S8N1cAStu7BOeFfE8KAQzmyyLkK8p/vmRq6kuBTW58Y= -cloud.google.com/go/storage v1.23.0/go.mod h1:vOEEDNFnciUMhBeT6hsJIn3ieU5cFRmzeLgDvXzfIXc= -contrib.go.opencensus.io/exporter/stackdriver v0.13.4/go.mod h1:aXENhDJ1Y4lIg4EUaVTwzvYETVNZk10Pu26tevFKLUc= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/14rcole/gopopulate v0.0.0-20180821133914-b175b219e774 h1:SCbEWT58NSt7d2mcFdvxC9uyrdcTfvBbPLThhkDmXzg= github.com/14rcole/gopopulate v0.0.0-20180821133914-b175b219e774/go.mod h1:6/0dYRLLXyJjbkIPeeGyoJ/eKOSI0eU6eTlCBYibgd0= -github.com/Antonboom/errname v0.1.5/go.mod h1:DugbBstvPFQbv/5uLcRRzfrNqKE9tVdVCqWCLp6Cifo= -github.com/Antonboom/nilnil v0.1.0/go.mod h1:PhHLvRPSghY5Y7mX4TW+BHZQYo1A8flE5H20D3IPZBo= github.com/Azure/azure-sdk-for-go v16.2.1+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= -github.com/Azure/azure-sdk-for-go v66.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= -github.com/Azure/go-ansiterm v0.0.0-20210608223527-2377c96fe795/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= -github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Azure/go-autorest v10.8.1+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= github.com/Azure/go-autorest/autorest v0.11.1/go.mod h1:JFgpikqFJ/MleTTxwepExTKnFUKKszPS8UavbQYUMuw= -github.com/Azure/go-autorest/autorest v0.11.24/go.mod h1:G6kyRlFnTuSbEYkQGawPfsCswgme4iYf6rfSKUDzbCc= -github.com/Azure/go-autorest/autorest v0.11.28/go.mod h1:MrkzG3Y3AH668QyF9KRk5neJnGgmhQ6krbhR8Q5eMvA= github.com/Azure/go-autorest/autorest/adal v0.9.0/go.mod h1:/c022QCutn2P7uY+/oQWWNcK9YU+MH96NgK+jErpbcg= github.com/Azure/go-autorest/autorest/adal v0.9.5/go.mod h1:B7KF7jKIeC9Mct5spmyCB/A8CG/sEz1vwIRGv/bbw7A= -github.com/Azure/go-autorest/autorest/adal v0.9.18/go.mod h1:XVVeme+LZwABT8K5Lc3hA4nAe8LDBVle26gTrguhhPQ= -github.com/Azure/go-autorest/autorest/adal v0.9.20/go.mod h1:XVVeme+LZwABT8K5Lc3hA4nAe8LDBVle26gTrguhhPQ= -github.com/Azure/go-autorest/autorest/azure/auth v0.5.11/go.mod h1:84w/uV8E37feW2NCJ08uT9VBfjfUHpgLVnG2InYD6cg= -github.com/Azure/go-autorest/autorest/azure/cli v0.4.5/go.mod h1:ADQAXrkgm7acgWVUNamOgh8YNrv4p27l3Wc55oVfpzg= github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= github.com/Azure/go-autorest/autorest/mocks v0.4.0/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= -github.com/Azure/go-autorest/autorest/mocks v0.4.2/go.mod h1:Vy7OitM9Kei0i1Oj+LvyAWMXJHeKH1MVlzFugfVrmyU= -github.com/Azure/go-autorest/autorest/to v0.4.0/go.mod h1:fE8iZBn7LQR7zH/9XU2NcPR4o9jEImooCeWJcYV/zLE= -github.com/Azure/go-autorest/autorest/validation v0.3.1/go.mod h1:yhLgjC0Wda5DYXl6JAsWyUe4KVNffhoDhG0zVzUMo3E= github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= -github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= -github.com/BurntSushi/toml v1.2.0 h1:Rt8g24XnyGTyglgET/PRUNlrUeu9F5L+7FilkXfZgs0= -github.com/BurntSushi/toml v1.2.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak= +github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= -github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= -github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= -github.com/DataDog/zstd v1.5.0/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= -github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24/go.mod h1:4UJr5HIiMZrwgkSPdsjy2uOQExX/WEILpIrO9UPGuXs= -github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= -github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= -github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= -github.com/Masterminds/semver/v3 v3.0.3/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= -github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= -github.com/Masterminds/sprig v2.15.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= -github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA= github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw= @@ -124,8 +51,8 @@ github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugX github.com/Microsoft/go-winio v0.4.17-0.20210211115548-6eac466e5fa3/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= github.com/Microsoft/go-winio v0.4.17-0.20210324224401-5516f17a5958/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= github.com/Microsoft/go-winio v0.4.17/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= -github.com/Microsoft/go-winio v0.5.2 h1:a9IhgEQBCUEk6QCdml9CiJGhAws+YwffDHEMp1VMrpA= -github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= +github.com/Microsoft/go-winio v0.6.0 h1:slsWYD/zyx7lCXoZVlvQrj0hPTM1HI4+v1sIda2yDvg= +github.com/Microsoft/go-winio v0.6.0/go.mod h1:cTAf44im0RAYeL23bpB+fzCyDH2MJiz2BO69KH/soAE= github.com/Microsoft/hcsshim v0.8.6/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg= github.com/Microsoft/hcsshim v0.8.7-0.20190325164909-8abdbb8205e4/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg= github.com/Microsoft/hcsshim v0.8.7/go.mod h1:OHd7sQqRFrYd3RmSgbgji+ctCwkbq2wbEYNSzOYtcBQ= @@ -134,20 +61,17 @@ github.com/Microsoft/hcsshim v0.8.14/go.mod h1:NtVKoYxQuTLx6gEq0L96c9Ju4JbRJ4nY2 github.com/Microsoft/hcsshim v0.8.15/go.mod h1:x38A4YbHbdxJtc0sF6oIz+RG0npwSCAvn69iY6URG00= github.com/Microsoft/hcsshim v0.8.16/go.mod h1:o5/SZqmR7x9JNKsW3pu+nqHm0MF8vbA+VxGOoXdC600= github.com/Microsoft/hcsshim v0.8.21/go.mod h1:+w2gRZ5ReXQhFOrvSQeNfhrYB/dg3oDwTOcER2fw4I4= -github.com/Microsoft/hcsshim v0.9.3 h1:k371PzBuRrz2b+ebGuI2nVgVhgsVX60jMfSw80NECxo= -github.com/Microsoft/hcsshim v0.9.3/go.mod h1:7pLA8lDk46WKDWlVsENo92gC0XFa8rbKfyFRBqxEbCc= +github.com/Microsoft/hcsshim v0.9.6 h1:VwnDOgLeoi2du6dAznfmspNqTiwczvjv4K7NxuY9jsY= +github.com/Microsoft/hcsshim v0.9.6/go.mod h1:7pLA8lDk46WKDWlVsENo92gC0XFa8rbKfyFRBqxEbCc= github.com/Microsoft/hcsshim/test v0.0.0-20201218223536-d3e5debf77da/go.mod h1:5hlzMzRKMLyo42nCZ9oml8AdTlq/0cvIaBv6tK1RehU= github.com/Microsoft/hcsshim/test v0.0.0-20210227013316-43a75bb4edd3/go.mod h1:mw7qgWloBUl75W/gVH3cQszUg1+gUITj7D6NY7ywVnY= github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= -github.com/OpenPeeDeeP/depguard v1.0.1/go.mod h1:xsIw86fROiiwelg+jB2uM9PiKihMMmUx/1V+TNhjQvM= -github.com/ProtonMail/go-crypto v0.0.0-20220824120805-4b6e5c587895/go.mod h1:UBYPn8k0D56RtnR8RFQMjmh4KrZzWJ5o7Z9SYjossQ8= github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ= -github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8= github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1ow= github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8= @@ -156,44 +80,14 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuy github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/alexflint/go-filemutex v0.0.0-20171022225611-72bdc8eae2ae/go.mod h1:CgnQgUtFrFz9mxFNtED3jI5tLDjKlOM+oUF/sTk6ps0= -github.com/alexkohler/prealloc v1.0.0/go.mod h1:VetnK3dIgFBBKmg0YnD9F9x6Icjd+9cvfHR56wJVlKE= -github.com/andybalholm/brotli v1.0.2/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= -github.com/andybalholm/brotli v1.0.3/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= -github.com/antihax/optional v0.0.0-20180407024304-ca021399b1a6/go.mod h1:V8iCPQYkqmusNa815XgQio277wI47sdRh1dUOLdyC6Q= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= -github.com/aokoli/goutils v1.0.1/go.mod h1:SijmP0QR8LtwsmDs8Yii5Z/S4trXFGFC2oO5g9DP+DQ= -github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= -github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= -github.com/armon/go-metrics v0.3.9/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc= -github.com/armon/go-metrics v0.4.0/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4= -github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= -github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= -github.com/ashanbrown/forbidigo v1.2.0/go.mod h1:vVW7PEdqEFqapJe95xHkTfB1+XvZXBFg8t0sG2FIxmI= -github.com/ashanbrown/makezero v0.0.0-20210520155254-b6261585ddde/go.mod h1:oG9Dnez7/ESBqc4EdrdNlryeo7d0KcW1ftXHm7nU/UU= +github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= +github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d h1:Byv0BzEl3/e6D5CLfI0j/7hiIEtvGVFPCZ7Ei2oq8iQ= +github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/aws/aws-sdk-go v1.15.11/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0= -github.com/aws/aws-sdk-go v1.23.20/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= -github.com/aws/aws-sdk-go v1.25.37/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= -github.com/aws/aws-sdk-go v1.36.30/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= -github.com/aws/aws-sdk-go v1.44.96/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= -github.com/aws/aws-sdk-go-v2 v1.16.14/go.mod h1:s/G+UV29dECbF5rf+RNj1xhlmvoNurGSr+McVSRj59w= -github.com/aws/aws-sdk-go-v2/config v1.17.5/go.mod h1:H0cvPNDO3uExWts/9PDhD/0ne2esu1uaIulwn1vkwxM= -github.com/aws/aws-sdk-go-v2/credentials v1.12.18/go.mod h1:O7n/CPagQ33rfG6h7vR/W02ammuc5CrsSM22cNZp9so= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.15/go.mod h1:Oz2/qWINxIgSmoZT9adpxJy2UhpcOAI3TIyWgYMVSz0= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.21/go.mod h1:XsmHMV9c512xgsW01q7H0ut+UQQQpWX8QsFbdLHDwaU= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.15/go.mod h1:kjJ4CyD9M3Wq88GYg3IPfj67Rs0Uvz8aXK7MJ8BvE4I= -github.com/aws/aws-sdk-go-v2/internal/ini v1.3.22/go.mod h1:tltHVGy977LrSOgRR5aV9+miyno/Gul/uJNPKS7FzP4= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.15/go.mod h1:ZVJ7ejRl4+tkWMuCwjXoy0jd8fF5u3RCyWjSVjUIvQE= -github.com/aws/aws-sdk-go-v2/service/kms v1.18.9/go.mod h1:8sR6O18d56mlJf0VkYD7mOtrBoM//8eym7FcfG1t9Sc= -github.com/aws/aws-sdk-go-v2/service/sso v1.11.21/go.mod h1:q8nYq51W3gpZempYsAD83fPRlrOTMCwN+Ahg4BKFTXQ= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.13.3/go.mod h1:+IF75RMJh0+zqTGXGshyEGRsU2ImqWv6UuHGkHl6kEo= -github.com/aws/aws-sdk-go-v2/service/sts v1.16.17/go.mod h1:bQujK1n0V1D1Gz5uII1jaB1WDvhj4/T3tElsJnVXCR0= -github.com/aws/smithy-go v1.13.2/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA= -github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= -github.com/beeker1121/goque v1.0.3-0.20191103205551-d618510128af/go.mod h1:84CWnaDz4g1tEVnFLnuBigmGK15oPohy0RfvSN8d4eg= github.com/beorn7/perks v0.0.0-20160804104726-4c0e84591b9a/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= @@ -202,32 +96,20 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= github.com/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA= -github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM= -github.com/bkielbasa/cyclop v1.2.0/go.mod h1:qOI0yy6A7dYC4Zgsa72Ppm9kONl0RoIlPbzot9mhmeI= github.com/blang/semver v3.1.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= -github.com/blizzy78/varnamelen v0.3.0/go.mod h1:hbwRdBvoBqxk34XyQ6HA0UH3G0/1TKuv5AC4eaBT0Ec= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= -github.com/bombsimon/wsl/v3 v3.3.0/go.mod h1:st10JtZYLE4D5sC7b8xV4zTKZwAQjCH/Hy2Pm1FNZIc= -github.com/breml/bidichk v0.1.1/go.mod h1:zbfeitpevDUGI7V91Uzzuwrn4Vls8MoBMrwtt78jmso= github.com/bshuster-repo/logrus-logstash-hook v0.4.1/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk= github.com/buger/jsonparser v0.0.0-20180808090653-f4dd9f5a6b44/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8= github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50= github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= -github.com/butuzov/ireturn v0.1.1/go.mod h1:Wh6Zl3IMtTpaIKbmwzqi6olnM9ptYQxxVacMsOEFPoc= -github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= -github.com/cenkalti/backoff/v3 v3.0.0/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs= -github.com/cenkalti/backoff/v3 v3.2.2/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs= github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= -github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/charithe/durationcheck v0.0.9/go.mod h1:SSbRIBVfMjCi/kEB6K65XEA83D6prSM8ap1UCpNKtgg= -github.com/chavacava/garif v0.0.0-20210405164556-e8a0a408d6af/go.mod h1:Qjyv4H3//PWVzTeCezG2b9IRn6myJxJSr4TD/xo6ojU= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/checkpoint-restore/go-criu/v4 v4.1.0/go.mod h1:xUQBLp4RLc5zJtWY++yjOoMoB5lihDt7fai+75m+rGw= github.com/checkpoint-restore/go-criu/v5 v5.0.0/go.mod h1:cfwC0EG7HMUenopBsUf9d89JlCLQIfgVcNsNN0t6T2M= github.com/checkpoint-restore/go-criu/v5 v5.3.0/go.mod h1:E/eQpaFtUKGOOSEBZgmKAcn+zUUwWxqcaKZlF54wK8E= @@ -246,22 +128,11 @@ github.com/cilium/ebpf v0.2.0/go.mod h1:To2CFviqOWL/M0gIMsvSMlqe7em/l1ALkX1PyjrX github.com/cilium/ebpf v0.4.0/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs= github.com/cilium/ebpf v0.6.2/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs= github.com/cilium/ebpf v0.7.0/go.mod h1:/oI2+1shJiTGAMgl6/RgJr36Eo1jzrRcAWbcXO2usCA= -github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= -github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= -github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb/go.mod h1:ZjrT6AXHbDs86ZSdt/osfBi5qfexBrKUdONk989Wnk4= github.com/containerd/aufs v0.0.0-20200908144142-dab0cbea06f4/go.mod h1:nukgQABAEopAHvB6j7cnP5zJ+/3aVcE7hCYqvIwAHyE= github.com/containerd/aufs v0.0.0-20201003224125-76a6863f2989/go.mod h1:AkGGQs9NM2vtYHaUen+NljV0/baGCAPELGm2q9ZXpWU= github.com/containerd/aufs v0.0.0-20210316121734-20793ff83c97/go.mod h1:kL5kd6KM5TzQjR79jljyi4olc1Vrx6XBlcyj3gNv2PU= @@ -326,8 +197,8 @@ github.com/containerd/nri v0.0.0-20201007170849-eb1350a75164/go.mod h1:+2wGSDGFY github.com/containerd/nri v0.0.0-20210316161719-dbaa18c31c14/go.mod h1:lmxnXF6oMkbqs39FiCt1s0R2HSMhcLel9vNL3m4AaeY= github.com/containerd/nri v0.1.0/go.mod h1:lmxnXF6oMkbqs39FiCt1s0R2HSMhcLel9vNL3m4AaeY= github.com/containerd/stargz-snapshotter/estargz v0.4.1/go.mod h1:x7Q9dg9QYb4+ELgxmo4gBUeJB0tl5dqH1Sdz0nJU1QM= -github.com/containerd/stargz-snapshotter/estargz v0.12.0 h1:idtwRTLjk2erqiYhPWy2L844By8NRFYEwYHcXhoIWPM= -github.com/containerd/stargz-snapshotter/estargz v0.12.0/go.mod h1:AIQ59TewBFJ4GOPEQXujcrJ/EKxh5xXZegW1rkR1P/M= +github.com/containerd/stargz-snapshotter/estargz v0.13.0 h1:fD7AwuVV+B40p0d9qVkH/Au1qhp8hn/HWJHIYjpEcfw= +github.com/containerd/stargz-snapshotter/estargz v0.13.0/go.mod h1:m+9VaGJGlhCnrcEUod8mYumTmRgblwd3rC5UCEh2Yp0= github.com/containerd/ttrpc v0.0.0-20190828154514-0e0f228740de/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o= github.com/containerd/ttrpc v0.0.0-20190828172938-92c8520ef9f8/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o= github.com/containerd/ttrpc v0.0.0-20191028202541-4f1b8fe65a5c/go.mod h1:LPm1u0xBw8r8NOKoOdNMeVHSawSsltak+Ihv+etqsE8= @@ -348,42 +219,38 @@ github.com/containernetworking/cni v0.8.0/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ github.com/containernetworking/cni v0.8.1/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY= github.com/containernetworking/plugins v0.8.6/go.mod h1:qnw5mN19D8fIwkqW7oHHYDHVlzhJpcY6TQxn/fUyDDM= github.com/containernetworking/plugins v0.9.1/go.mod h1:xP/idU2ldlzN6m4p5LmGiwRDjeJr6FLK6vuiUwoH7P8= -github.com/containers/libtrust v0.0.0-20200511145503-9c3a6c22cd9a h1:spAGlqziZjCJL25C6F1zsQY05tfCKE9F5YwtEWWe6hU= -github.com/containers/libtrust v0.0.0-20200511145503-9c3a6c22cd9a/go.mod h1:9rfv8iPl1ZP7aqh9YA68wnZv2NUDbXdcdPHVz0pFbPY= +github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01 h1:Qzk5C6cYglewc+UyGf6lc8Mj2UaPTHy/iF2De0/77CA= +github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01/go.mod h1:9rfv8iPl1ZP7aqh9YA68wnZv2NUDbXdcdPHVz0pFbPY= github.com/containers/ocicrypt v1.0.1/go.mod h1:MeJDzk1RJHv89LjsH0Sp5KTY3ZYkjXO/C+bKAeWFIrc= github.com/containers/ocicrypt v1.1.0/go.mod h1:b8AOe0YR67uU8OqfVNcznfFpAzu3rdgUV4GP9qXPfu4= github.com/containers/ocicrypt v1.1.1/go.mod h1:Dm55fwWm1YZAjYRaJ94z2mfZikIyIN4B0oB3dj3jFxY= -github.com/containers/ocicrypt v1.1.5 h1:UO+gBnBXvMvC7HTXLh0bPgLslfW8HlY+oxYcoSHBcZQ= -github.com/containers/ocicrypt v1.1.5/go.mod h1:WgjxPWdTJMqYMjf3M6cuIFFA1/MpyyhIM99YInA+Rvc= -github.com/containers/storage v1.42.0 h1:zm2AQD4NDeTB3JQ8X+Wo5+VRqNB+b4ocEd7Qj6ylPJA= -github.com/containers/storage v1.42.0/go.mod h1:JiUJwOgOo1dr2DdOUc1MRe2GCAXABYoYmOdPF8yvH78= +github.com/containers/ocicrypt v1.1.7 h1:thhNr4fu2ltyGz8aMx8u48Ae0Pnbip3ePP9/mzkZ/3U= +github.com/containers/ocicrypt v1.1.7/go.mod h1:7CAhjcj2H8AYp5YvEie7oVSK2AhBY8NscCYRawuDNtw= +github.com/containers/storage v1.45.3 h1:GbtTvTtp3GW2/tcFg5VhgHXcYMwVn2KfZKiHjf9FAOM= +github.com/containers/storage v1.45.3/go.mod h1:OdRUYHrq1HP6iAo79VxqtYuJzC5j4eA2I60jKOoCT7g= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= -github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= github.com/coreos/go-iptables v0.4.5/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU= github.com/coreos/go-iptables v0.5.0/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU= github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= -github.com/coreos/go-oidc/v3 v3.4.0/go.mod h1:eHUXhZtXPQLgEaDrOVTgwbgmz1xGOkJNye6h3zkD2Pw= +github.com/coreos/go-oidc/v3 v3.5.0 h1:VxKtbccHZxs8juq7RdJntSqtXFtde9YpNpGn0yqgEHw= +github.com/coreos/go-oidc/v3 v3.5.0/go.mod h1:ecXRtV4romGPeO6ieExAsUK9cb/3fp9hXNz1tlv8PIM= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20161114122254-48702e0da86b/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/go-systemd v0.0.0-20190620071333-e64a0ec8b42a/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd/v22 v22.0.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk= github.com/coreos/go-systemd/v22 v22.1.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk= github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= -github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/cyberphone/json-canonicalization v0.0.0-20220623050100-57a0ce2678a7 h1:vU+EP9ZuFUCYE0NYLwTSob+3LNEJATzNfP/DC7SWGWI= +github.com/cyberphone/json-canonicalization v0.0.0-20220623050100-57a0ce2678a7/go.mod h1:uzvlm1mxhHkdfqitSA92i7Se+S9ksOn3a3qmv/kyOCw= github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4= github.com/cyphar/filepath-securejoin v0.2.3 h1:YX6ebbZCZP7VkM3scTTokDgBL2TY741X51MTk3ycuNI= github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= @@ -391,215 +258,163 @@ github.com/d2g/dhcp4 v0.0.0-20170904100407-a1d1b6c41b1c/go.mod h1:Ct2BUK8SB0YC1S github.com/d2g/dhcp4client v1.0.0/go.mod h1:j0hNfjhrt2SxUOw55nL0ATM/z4Yt3t2Kd1mW34z5W5s= github.com/d2g/dhcp4server v0.0.0-20181031114812-7d4a0a7f59a5/go.mod h1:Eo87+Kg/IX2hfWJfwxMzLyuSZyxSoAug2nGa1G2QAi8= github.com/d2g/hardwareaddr v0.0.0-20190221164911-e7d9fbe030e4/go.mod h1:bMl4RjIciD2oAxI7DmWRx6gbeqrkoLqv3MV0vzNad+I= -github.com/daixiang0/gci v0.2.9/go.mod h1:+4dZ7TISfSmqfAGv59ePaHfNzgGtIkHAhhdKggP1JAc= -github.com/danieljoos/wincred v1.1.0/go.mod h1:XYlo+eRTsVA9aHGp7NGjFkPla4m+DCL7hqDjlFjiygg= -github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/denis-tingajkin/go-header v0.4.2/go.mod h1:eLRHAVXzE5atsKAnNRDB90WHCFFnBUn4RN0nRcs1LJA= github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba/go.mod h1:dV8lFg6daOBZbT6/BDGIz6Y3WFGn8juu6G+CQ6LHtl0= github.com/dgrijalva/jwt-go v0.0.0-20170104182250-a601269ab70c/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= -github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= -github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE= github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= github.com/docker/cli v0.0.0-20191017083524-a8ff7f821017/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= -github.com/docker/cli v20.10.17+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/distribution v0.0.0-20190905152932-14b96e55d84c/go.mod h1:0+TTO4EOBfRPhZXAeF1Vu+W3hHZ8eLp8PgKVZlcvtFY= github.com/docker/distribution v2.7.1-0.20190205005809-0d3efadf0154+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/distribution v2.8.1+incompatible h1:Q50tZOPR6T/hjNsyc9g8/syEs6bk8XXApsHjKukMl68= github.com/docker/distribution v2.8.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/docker v1.4.2-0.20190924003213-a8608b5b67c7/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/docker v20.10.17+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/docker v20.10.18+incompatible h1:SN84VYXTBNGn92T/QwIRPlum9zfemfitN7pbsp26WSc= -github.com/docker/docker v20.10.18+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v23.0.1+incompatible h1:vjgvJZxprTTE1A37nm+CLNAdwu6xZekyoiVlUZEINcY= +github.com/docker/docker v23.0.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker-credential-helpers v0.6.3/go.mod h1:WRaJzqw3CTB9bk10avuGsjVBZsD05qeibJ1/TYlvc0Y= -github.com/docker/docker-credential-helpers v0.6.4 h1:axCks+yV+2MR3/kZhAmy07yC56WZ2Pwu/fKWtKuZB0o= -github.com/docker/docker-credential-helpers v0.6.4/go.mod h1:ofX3UI0Gz1TteYBjtgs07O36Pyasyp66D2uKT7H8W1c= +github.com/docker/docker-credential-helpers v0.7.0 h1:xtCHsjxogADNZcdv1pKUHXryefjlVRqWqIhk/uXJp0A= +github.com/docker/docker-credential-helpers v0.7.0/go.mod h1:rETQfLdHNT3foU5kuNkFR1R1V12OJRRO5lzt2D1b5X0= github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= github.com/docker/go-events v0.0.0-20170721190031-9461782956ad/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= github.com/docker/go-metrics v0.0.0-20180209012529-399ea8c73916/go.mod h1:/u0gXw0Gay3ceNrsHubL3BtdOL2fHf93USgMTe0W5dI= github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= -github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= -github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 h1:UhxFibDNY/bfvqU5CAUmr9zpesgbU6SWc8/B4mflAE4= -github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/eggsampler/acme/v3 v3.3.0/go.mod h1:/qh0rKC/Dh7Jj+p4So7DbWmFNzC4dpcpK53r226Fhuo= github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= -github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= -github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= -github.com/envoyproxy/protoc-gen-validate v0.0.14/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/esimonov/ifshort v1.0.3/go.mod h1:yZqNJUrNn20K8Q9n2CrjTKYyVEmX209Hgu+M1LBpeZE= -github.com/ettle/strcase v0.1.1/go.mod h1:hzDLsPC7/lwKyBOywSHEP89nt2pDgdy+No1NBA9o9VY= github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= -github.com/evanphx/json-patch/v5 v5.5.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4= github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a h1:yDWHCSQ40h88yih2JAcL6Ls/kVkSE8GFACTGVnMPruw= -github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a/go.mod h1:7Ga40egUymuWXxAe151lTNnCv97MddSOVsjpPPkityA= -github.com/facebookgo/ensure v0.0.0-20200202191622-63f1cf65ac4c/go.mod h1:Yg+htXGokKKdzcwhuNDwVvN+uBxDGXJ7G/VN1d8fa64= github.com/facebookgo/limitgroup v0.0.0-20150612190941-6abd8d71ec01 h1:IeaD1VDVBPlx3viJT9Md8if8IxxJnO+x0JCGb054heg= -github.com/facebookgo/limitgroup v0.0.0-20150612190941-6abd8d71ec01/go.mod h1:ypD5nozFk9vcGw1ATYefw6jHe/jZP++Z15/+VTMcWhc= github.com/facebookgo/muster v0.0.0-20150708232844-fd3d7953fd52 h1:a4DFiKFJiDRGFD1qIcqGLX/WlUMD9dyLSLDt+9QZgt8= -github.com/facebookgo/muster v0.0.0-20150708232844-fd3d7953fd52/go.mod h1:yIquW87NGRw1FU5p5lEkpnt/QxoH5uPAOUlOVkAUuMg= -github.com/facebookgo/stack v0.0.0-20160209184415-751773369052/go.mod h1:UbMTZqLaRiH3MsBH8va0n7s1pQYcu3uTb8G4tygF4Zg= -github.com/facebookgo/subset v0.0.0-20200203212716-c811ad88dec4/go.mod h1:5tD+neXqOorC30/tWg0LCSkrqj/AR6gu8yY8/fpw1q0= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= -github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= -github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= -github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= -github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94= -github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/flynn/go-docopt v0.0.0-20140912013429-f6dd2ebbb31e/go.mod h1:HyVoz1Mz5Co8TFO8EupIdlcpwShBmY98dkT2xeHkvEI= +github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= -github.com/frankban/quicktest v1.10.0/go.mod h1:ui7WezCLWMWxVWr1GETZY3smRy0G4KWq9vcPtJmFl7Y= github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= -github.com/frankban/quicktest v1.13.0/go.mod h1:qLE0fzW0VuyUAJgPU19zByoIr0HtCHN/r/VLSOOIySU= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= -github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa/go.mod h1:KnogPXtdwXqoenmZCw6S+25EAm2MkxbG0deNDu4cbSA= -github.com/fullstorydev/grpcurl v1.6.0/go.mod h1:ZQ+ayqbKMJNhzLmbpCiurTVlaK2M/3nqZCxaQ2Ze/sM= -github.com/fzipp/gocyclo v0.3.1/go.mod h1:DJHO6AUmbdqj2ET4Z9iArSuwWgYDRryYt2wASxc7x3E= github.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY= github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= -github.com/gin-gonic/gin v1.7.1/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY= -github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ1qq1U= -github.com/go-asn1-ber/asn1-ber v1.3.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= -github.com/go-critic/go-critic v0.6.1/go.mod h1:SdNCfU0yF3UBjtaZGw6586/WocupMOJuiqgom5DsQxM= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-gorp/gorp/v3 v3.0.2/go.mod h1:BJ3q1ejpV8cVALtcXvXaXyTOlMmJhWDxTmncaR6rwBY= github.com/go-ini/ini v1.25.4/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= +github.com/go-jose/go-jose/v3 v3.0.0 h1:s6rrhirfEP/CGIoc6p+PZAeogN2SxKav6Wp7+dyMWVo= +github.com/go-jose/go-jose/v3 v3.0.0/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= -github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= -github.com/go-ldap/ldap/v3 v3.1.10/go.mod h1:5Zun81jBTabRaI8lzN7E1JjyEl1g6zI6u9pd8luAK4Q= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= -github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= -github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= -github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= -github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= +github.com/go-openapi/analysis v0.21.2/go.mod h1:HZwRk4RRisyG8vx2Oe6aqeSQcoxRp47Xkp3+K6q+LdY= +github.com/go-openapi/analysis v0.21.4 h1:ZDFLvSNxpDaomuCueM0BlSXxpANBlFYiBvr+GXrvIHc= +github.com/go-openapi/analysis v0.21.4/go.mod h1:4zQ35W4neeZTqh3ol0rv/O8JBbka9QyAgQRPp9y3pfo= +github.com/go-openapi/errors v0.19.8/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= +github.com/go-openapi/errors v0.19.9/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= +github.com/go-openapi/errors v0.20.2/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= +github.com/go-openapi/errors v0.20.3 h1:rz6kiC84sqNQoqrtulzaL/VERgkoCyB6WdEkc2ujzUc= +github.com/go-openapi/errors v0.20.3/go.mod h1:Z3FlZ4I8jEGxjUK+bugx3on2mIAk4txuAOhlsB1FSgk= github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0= github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg= github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= +github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns= +github.com/go-openapi/jsonreference v0.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA= +github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo= +github.com/go-openapi/loads v0.21.1/go.mod h1:/DtAMXXneXFjbQMGEtbamCZb+4x7eGwkvZCvBmwUG+g= +github.com/go-openapi/loads v0.21.2 h1:r2a/xFIYeZ4Qd2TnGpWDIQNcP80dIaZgf704za8enro= +github.com/go-openapi/loads v0.21.2/go.mod h1:Jq58Os6SSGz0rzh62ptiu8Z31I+OTHqmULx5e/gJbNw= +github.com/go-openapi/runtime v0.24.1 h1:Sml5cgQKGYQHF+M7yYSHaH1eOjvTykrddTE/KtQVjqo= +github.com/go-openapi/runtime v0.24.1/go.mod h1:AKurw9fNre+h3ELZfk6ILsfvPN+bvvlaU/M9q/r9hpk= github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc= github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= +github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I= +github.com/go-openapi/spec v0.20.6/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA= +github.com/go-openapi/spec v0.20.7 h1:1Rlu/ZrOCCob0n+JKKJAWhNWMPW8bOZRg8FJaY+0SKI= +github.com/go-openapi/spec v0.20.7/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA= +github.com/go-openapi/strfmt v0.21.0/go.mod h1:ZRQ409bWMj+SOgXofQAGTIo2Ebu72Gs+WaRADcS5iNg= +github.com/go-openapi/strfmt v0.21.1/go.mod h1:I/XVKeLc5+MM5oPNN7P6urMOpuLXEcNrCX/rPGuWb0k= +github.com/go-openapi/strfmt v0.21.2/go.mod h1:I/XVKeLc5+MM5oPNN7P6urMOpuLXEcNrCX/rPGuWb0k= +github.com/go-openapi/strfmt v0.21.3 h1:xwhj5X6CjXEZZHMWy1zKJxvW9AfHC9pkyUjLvHtKG7o= +github.com/go-openapi/strfmt v0.21.3/go.mod h1:k+RzNO0Da+k3FrrynSNN8F7n/peCmQQqbbXjtDfvmGg= github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I= github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-openapi/swag v0.21.1/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-openapi/validate v0.21.0/go.mod h1:rjnrwK57VJ7A8xqfpAOEKRH8yQSGUriMu5/zuPSQ1hg= +github.com/go-openapi/validate v0.22.0 h1:b0QecH6VslW/TxtpKgzpO1SNG7GU2FsaqKdP1E2T50Y= +github.com/go-openapi/validate v0.22.0/go.mod h1:rjnrwK57VJ7A8xqfpAOEKRH8yQSGUriMu5/zuPSQ1hg= +github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= -github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= -github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= -github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= -github.com/go-redis/redis v6.15.8+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= -github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= -github.com/go-rod/rod v0.109.3/go.mod h1:GZDtmEs6RpF6kBRYpGCZXxXlKNneKVPiKOjaMbmVVjE= -github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= -github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= -github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= -github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU= +github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= +github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho= +github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= +github.com/go-playground/validator/v10 v10.11.1 h1:prmOlTVv+YjZjmRmNSF3VmspqJIxJWXmqUsHwfTRRkQ= +github.com/go-playground/validator/v10 v10.11.1/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU= +github.com/go-rod/rod v0.112.3 h1:xbSaA9trZ8v/+eJRGOM6exK1RCsLPwwnzA78vpES0gk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= -github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= -github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= -github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= -github.com/go-toolsmith/astcast v1.0.0/go.mod h1:mt2OdQTeAQcY4DQgPSArJjHCcOwlX+Wl/kwN+LbLGQ4= -github.com/go-toolsmith/astcopy v1.0.0/go.mod h1:vrgyG+5Bxrnz4MZWPF+pI4R8h3qKRjjyvV/DSez4WVQ= -github.com/go-toolsmith/astequal v1.0.0/go.mod h1:H+xSiq0+LtiDC11+h1G32h7Of5O3CYFJ99GVbS5lDKY= -github.com/go-toolsmith/astequal v1.0.1/go.mod h1:4oGA3EZXTVItV/ipGiOx7NWkY5veFfcsOJVS2YxltLw= -github.com/go-toolsmith/astfmt v1.0.0/go.mod h1:cnWmsOAuq4jJY6Ct5YWlVLmcmLMn1JUPuQIHCY7CJDw= -github.com/go-toolsmith/astinfo v0.0.0-20180906194353-9809ff7efb21/go.mod h1:dDStQCHtmZpYOmjRP/8gHHnCCch3Zz3oEgCdZVdtweU= -github.com/go-toolsmith/astp v1.0.0/go.mod h1:RSyrtpVlfTFGDYRbrjyWP1pYu//tSFcvdYrA8meBmLI= -github.com/go-toolsmith/pkgload v1.0.0/go.mod h1:5eFArkbO80v7Z0kdngIxsRXRMTaX4Ilcwuh3clNrQJc= -github.com/go-toolsmith/strparse v1.0.0/go.mod h1:YI2nUKP9YGZnL/L1/DLFBfixrcjslWct4wyljWhSRy8= -github.com/go-toolsmith/typep v1.0.0/go.mod h1:JSQCQMUPdRlMZFswiq3TGpNp1GMktqkR2Ns5AIQkATU= -github.com/go-toolsmith/typep v1.0.2/go.mod h1:JSQCQMUPdRlMZFswiq3TGpNp1GMktqkR2Ns5AIQkATU= -github.com/go-xmlfmt/xmlfmt v0.0.0-20191208150333-d5b6f63a941b/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM= -github.com/gobuffalo/attrs v0.1.0/go.mod h1:fmNpaWyHM0tRm8gCZWKx8yY9fvaNLo2PyzBNSrBZ5Hw= -github.com/gobuffalo/attrs v1.0.1/go.mod h1:qGdnq2RukKtBl4ASJit0OFckc5XGSyTFk98SvRpMFrQ= -github.com/gobuffalo/envy v1.8.1/go.mod h1:FurDp9+EDPE4aIUS3ZLyD+7/9fpx7YRt/ukY6jIHf0w= -github.com/gobuffalo/envy v1.9.0/go.mod h1:FurDp9+EDPE4aIUS3ZLyD+7/9fpx7YRt/ukY6jIHf0w= -github.com/gobuffalo/envy v1.10.1/go.mod h1:AWx4++KnNOW3JOeEvhSaq+mvgAvnMYOY1XSIin4Mago= -github.com/gobuffalo/fizz v1.10.0/go.mod h1:J2XGPO0AfJ1zKw7+2BA+6FEGAkyEsdCOLvN93WCT2WI= -github.com/gobuffalo/fizz v1.14.0/go.mod h1:0aF1kAZYCfKqbLM/lmZ3jXFyqqWE/kY/nIOKnNdAYXQ= -github.com/gobuffalo/flect v0.1.5/go.mod h1:W3K3X9ksuZfir8f/LrfVtWmCDQFfayuylOJ7sz/Fj80= -github.com/gobuffalo/flect v0.2.0/go.mod h1:W3K3X9ksuZfir8f/LrfVtWmCDQFfayuylOJ7sz/Fj80= -github.com/gobuffalo/flect v0.2.1/go.mod h1:vmkQwuZYhN5Pc4ljYQZzP+1sq+NEkK+lh20jmEmX3jc= -github.com/gobuffalo/flect v0.2.4/go.mod h1:1ZyCLIbg0YD7sDkzvFdPoOydPtD8y9JQnrOROolUcM8= -github.com/gobuffalo/flect v0.2.5/go.mod h1:1ZyCLIbg0YD7sDkzvFdPoOydPtD8y9JQnrOROolUcM8= -github.com/gobuffalo/genny/v2 v2.0.5/go.mod h1:kRkJuAw9mdI37AiEYjV4Dl+TgkBDYf8HZVjLkqe5eBg= -github.com/gobuffalo/genny/v2 v2.0.8/go.mod h1:R45scCyQfff2HysNJHNanjrpvPw4Qu+rM1MOMDBB5oU= -github.com/gobuffalo/genny/v2 v2.0.9/go.mod h1:R45scCyQfff2HysNJHNanjrpvPw4Qu+rM1MOMDBB5oU= -github.com/gobuffalo/github_flavored_markdown v1.1.0/go.mod h1:TSpTKWcRTI0+v7W3x8dkSKMLJSUpuVitlptCkpeY8ic= -github.com/gobuffalo/github_flavored_markdown v1.1.1/go.mod h1:yU32Pen+eorS58oxh/bNZx76zUOCJwmvyV5FBrvzOKQ= -github.com/gobuffalo/helpers v0.6.0/go.mod h1:pncVrer7x/KRvnL5aJABLAuT/RhKRR9klL6dkUOhyv8= -github.com/gobuffalo/helpers v0.6.1/go.mod h1:wInbDi0vTJKZBviURTLRMFLE4+nF2uRuuL2fnlYo7w4= -github.com/gobuffalo/helpers v0.6.4/go.mod h1:m2aOKsTl3KB0RUwwpxf3tykaaitujQ3irivqrlNAcJ0= -github.com/gobuffalo/logger v1.0.3/go.mod h1:SoeejUwldiS7ZsyCBphOGURmWdwUFXs0J7TCjEhjKxM= -github.com/gobuffalo/logger v1.0.6/go.mod h1:J31TBEHR1QLV2683OXTAItYIg8pv2JMHnF/quuAbMjs= -github.com/gobuffalo/nulls v0.2.0/go.mod h1:w4q8RoSCEt87Q0K0sRIZWYeIxkxog5mh3eN3C/n+dUc= -github.com/gobuffalo/nulls v0.4.1/go.mod h1:pp8e1hWTRJZFpMl4fj/CVbSMlaxjeGKkFq4RuBZi3w8= -github.com/gobuffalo/packd v0.3.0/go.mod h1:zC7QkmNkYVGKPw4tHpBQ+ml7W/3tIebgeo1b36chA3Q= -github.com/gobuffalo/packd v1.0.0/go.mod h1:6VTc4htmJRFB7u1m/4LeMTWjFoYrUiBkU9Fdec9hrhI= -github.com/gobuffalo/packd v1.0.1/go.mod h1:PP2POP3p3RXGz7Jh6eYEf93S7vA2za6xM7QT85L4+VY= -github.com/gobuffalo/packr/v2 v2.8.0/go.mod h1:PDk2k3vGevNE3SwVyVRgQCCXETC9SaONCNSXT1Q8M1g= -github.com/gobuffalo/plush/v4 v4.0.0/go.mod h1:ErFS3UxKqEb8fpFJT7lYErfN/Nw6vHGiDMTjxpk5bQ0= -github.com/gobuffalo/plush/v4 v4.1.9/go.mod h1:9OOII9uAM5pZnhWu1OkQnboXJjaWMQ7kcTl3zNcxvTM= -github.com/gobuffalo/plush/v4 v4.1.11/go.mod h1:9OOII9uAM5pZnhWu1OkQnboXJjaWMQ7kcTl3zNcxvTM= -github.com/gobuffalo/pop/v5 v5.3.1/go.mod h1:vcEDhh6cJ3WVENqJDFt/6z7zNb7lLnlN8vj3n5G9rYA= -github.com/gobuffalo/pop/v6 v6.0.0/go.mod h1:5rd3OnViLhjteR8+0i/mT9Q4CzkTzCoR7tm/9mmAic4= -github.com/gobuffalo/pop/v6 v6.0.4/go.mod h1:dFcrMNPOwk+sl1Oa0lOb/jGbmjv+JV+5CZjMWNYR3KI= -github.com/gobuffalo/tags/v3 v3.0.2/go.mod h1:ZQeN6TCTiwAFnS0dNcbDtSgZDwNKSpqajvVtt6mlYpA= -github.com/gobuffalo/tags/v3 v3.1.0/go.mod h1:ZQeN6TCTiwAFnS0dNcbDtSgZDwNKSpqajvVtt6mlYpA= -github.com/gobuffalo/tags/v3 v3.1.2/go.mod h1:o3ldUfKv50jxWAC8eZHXMm8dnKW3YvyZUMr0xqUcZTI= -github.com/gobuffalo/validate/v3 v3.0.0/go.mod h1:HFpjq+AIiA2RHoQnQVTFKF/ZpUPXwyw82LgyDPxQ9r0= -github.com/gobuffalo/validate/v3 v3.1.0/go.mod h1:HFpjq+AIiA2RHoQnQVTFKF/ZpUPXwyw82LgyDPxQ9r0= -github.com/gobuffalo/validate/v3 v3.3.1/go.mod h1:Ehu8ieNJQuUM4peDDr/0VapzdGA7RgTc3wbe51vHfS0= -github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4= +github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg= +github.com/gobuffalo/attrs v0.0.0-20190224210810-a9411de4debd/go.mod h1:4duuawTqi2wkkpB4ePgWMaai6/Kc6WEz83bhFwpHzj0= +github.com/gobuffalo/depgen v0.0.0-20190329151759-d478694a28d3/go.mod h1:3STtPUQYuzV0gBVOY3vy6CfMm/ljR4pABfrTeHNLHUY= +github.com/gobuffalo/depgen v0.1.0/go.mod h1:+ifsuy7fhi15RWncXQQKjWS9JPkdah5sZvtHc2RXGlg= +github.com/gobuffalo/envy v1.6.15/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= +github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= +github.com/gobuffalo/flect v0.1.0/go.mod h1:d2ehjJqGOH/Kjqcoz+F7jHTBbmDb38yXA598Hb50EGs= +github.com/gobuffalo/flect v0.1.1/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI= +github.com/gobuffalo/flect v0.1.3/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI= +github.com/gobuffalo/genny v0.0.0-20190329151137-27723ad26ef9/go.mod h1:rWs4Z12d1Zbf19rlsn0nurr75KqhYp52EAGGxTbBhNk= +github.com/gobuffalo/genny v0.0.0-20190403191548-3ca520ef0d9e/go.mod h1:80lIj3kVJWwOrXWWMRzzdhW3DsrdjILVil/SFKBzF28= +github.com/gobuffalo/genny v0.1.0/go.mod h1:XidbUqzak3lHdS//TPu2OgiFB+51Ur5f7CSnXZ/JDvo= +github.com/gobuffalo/genny v0.1.1/go.mod h1:5TExbEyY48pfunL4QSXxlDOmdsD44RRq4mVZ0Ex28Xk= +github.com/gobuffalo/gitgen v0.0.0-20190315122116-cc086187d211/go.mod h1:vEHJk/E9DmhejeLeNt7UVvlSGv3ziL+djtTr3yyzcOw= +github.com/gobuffalo/gogen v0.0.0-20190315121717-8f38393713f5/go.mod h1:V9QVDIxsgKNZs6L2IYiGR8datgMhB577vzTDqypH360= +github.com/gobuffalo/gogen v0.1.0/go.mod h1:8NTelM5qd8RZ15VjQTFkAW6qOMx5wBbW4dSCS3BY8gg= +github.com/gobuffalo/gogen v0.1.1/go.mod h1:y8iBtmHmGc4qa3urIyo1shvOD8JftTtfcKi+71xfDNE= +github.com/gobuffalo/logger v0.0.0-20190315122211-86e12af44bc2/go.mod h1:QdxcLw541hSGtBnhUc4gaNIXRjiDppFGaDqzbrBd3v8= +github.com/gobuffalo/mapi v1.0.1/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc= +github.com/gobuffalo/mapi v1.0.2/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc= +github.com/gobuffalo/packd v0.0.0-20190315124812-a385830c7fc0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4= +github.com/gobuffalo/packd v0.1.0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4= +github.com/gobuffalo/packr/v2 v2.0.9/go.mod h1:emmyGweYTm6Kdper+iywB6YK5YzuKchGtJQZ0Odn4pQ= +github.com/gobuffalo/packr/v2 v2.2.0/go.mod h1:CaAwI0GPIAv+5wKLtv8Afwl+Cm78K/I/VCm/3ptBN+0= +github.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw= github.com/godbus/dbus v0.0.0-20151105175453-c7fdd8b5cd55/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= github.com/godbus/dbus v0.0.0-20180201030542-885f9cc04c9c/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= -github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= -github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= -github.com/gofrs/uuid v4.1.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= -github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/googleapis v1.2.0/go.mod h1:Njal3psf3qN6dwBtQfUmBZh2ybovJ0tlu3o/AC7HYjU= github.com/gogo/googleapis v1.4.0/go.mod h1:5YRNX2z1oM5gXdAkurHa942MDgEJyk02w4OecKY87+c= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= @@ -609,10 +424,6 @@ github.com/gogo/protobuf v1.3.0/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXP github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= -github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= -github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= -github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -627,10 +438,7 @@ github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFU github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= -github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= -github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -646,32 +454,15 @@ github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QD github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2/go.mod h1:k9Qvh+8juN+UKMCS/3jFtGICgW8O96FVaZsaxdzDkR4= -github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a/go.mod h1:ryS0uhF+x9jgbj/N71xsEqODy9BN81/GonCZiOzirOk= -github.com/golangci/go-misc v0.0.0-20180628070357-927a3d87b613/go.mod h1:SyvUF2NxV+sN8upjjeVYr5W7tyxaT1JVtvhKhOn2ii8= -github.com/golangci/gofmt v0.0.0-20190930125516-244bba706f1a/go.mod h1:9qCChq59u/eW8im404Q2WWTrnBUQKjpNYKMbU4M7EFU= -github.com/golangci/golangci-lint v1.43.0/go.mod h1:VIFlUqidx5ggxDfQagdvd9E67UjMXtTHBkBQ7sHoC5Q= -github.com/golangci/lint-1 v0.0.0-20191013205115-297bf364a8e0/go.mod h1:66R6K6P6VWk9I95jvqGxkqJxVWGFy9XlDwLwVz1RCFg= -github.com/golangci/maligned v0.0.0-20180506175553-b1d89398deca/go.mod h1:tvlJhZqDe4LMs4ZHD0oMUlt9G2LWuDGoisJTBzLMV9o= -github.com/golangci/misspell v0.3.5/go.mod h1:dEbvlSfYbMQDtrpRMQU675gSDLDNa8sCPPChZ7PhiVA= -github.com/golangci/revgrep v0.0.0-20210930125155-c22e5001d4f2/go.mod h1:LK+zW4MpyytAWQRz0M4xnzEk50lSvqDQKfx304apFkY= -github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4/go.mod h1:Izgrg8RkN3rCIMLGE9CyYmU9pY2Jer6DgANEnZ/L/cQ= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/certificate-transparency-go v1.0.21/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg= -github.com/google/certificate-transparency-go v1.0.22-0.20181127102053-c25855a82c75/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg= -github.com/google/certificate-transparency-go v1.1.1/go.mod h1:FDKqPvSXawb2ecErVRrD+nfy23RCzyl7eqVCEmlT1Zs= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= @@ -679,64 +470,35 @@ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-containerregistry v0.5.1/go.mod h1:Ct15B4yir3PLOP5jsy0GNeYVaIZs/MK/Jz5any1wFW0= -github.com/google/go-containerregistry v0.11.0 h1:Xt8x1adcREjFcmDoDK8OdOsjxu90PHkGuwNP8GiHMLM= -github.com/google/go-containerregistry v0.11.0/go.mod h1:BBaYtsHPHA42uEgAvd/NejvAfPSlz281sJWqupjSxfk= +github.com/google/go-containerregistry v0.12.1 h1:W1mzdNUTx4Zla4JaixCRLhORcR7G6KxE5hHl5fkPsp8= +github.com/google/go-containerregistry v0.12.1/go.mod h1:sdIK+oHQO7B93xI8UweYdl887YhuIwg9vz8BSLH3+8k= github.com/google/go-intervals v0.0.2 h1:FGrVEiUnTRKR8yE04qzXYaJMtnIYqobR5QbblK3ixcM= github.com/google/go-intervals v0.0.2/go.mod h1:MkaR3LNRfeKLPmqgJYs4E66z5InYjmCjbbr4TQlcT6Y= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200507031123-427632fa3b1c/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/trillian v1.3.11/go.mod h1:0tPraVHrSDkA3BO6vKX67zgLXs6SsOAbHEivX+9mPgw= -github.com/google/uuid v0.0.0-20161128191214-064e2069ce9c/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/trillian v1.5.1-0.20220819043421-0a389c4bb8d9 h1:GFmzYtwUMi1S2mjLxfrJ/CZ9gWDG+zeLtZByg/QEBkk= +github.com/google/trillian v1.5.1-0.20220819043421-0a389c4bb8d9/go.mod h1:vywkS3p2SgNmPL7oAWqU5PiiknzRMp+ol3a19jfY2PQ= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= -github.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= -github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= -github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM= -github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM= -github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c= github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg= -github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4= -github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gordonklaus/ineffassign v0.0.0-20200309095847-7953dde2c7bf/go.mod h1:cuNKsD1zp2v6XfE/orVX2QE1LC+i254ceGcVeDT3pTU= -github.com/gordonklaus/ineffassign v0.0.0-20210225214923-2e10b2664254/go.mod h1:M9mZEtGIsR1oDaZagNPNG9iq9n2HrhZ17dsXk73V3Lw= -github.com/gorhill/cronexpr v0.0.0-20180427100037-88b0669f7d75/go.mod h1:g2644b03hfBX9Ov0ZBDgXXens4rxSxmqFBbhvKv2yVA= -github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= github.com/gorilla/handlers v0.0.0-20150720190736-60c7bfde3e33/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= github.com/gorilla/mux v1.7.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= @@ -744,110 +506,35 @@ github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= -github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/gostaticanalysis/analysisutil v0.0.0-20190318220348-4088753ea4d3/go.mod h1:eEOZF4jCKGi+aprrirO9e7WKB3beBRtWgqGunKl6pKE= -github.com/gostaticanalysis/analysisutil v0.0.3/go.mod h1:eEOZF4jCKGi+aprrirO9e7WKB3beBRtWgqGunKl6pKE= -github.com/gostaticanalysis/analysisutil v0.1.0/go.mod h1:dMhHRU9KTiDcuLGdy87/2gTR8WruwYZrKdRq9m1O6uw= -github.com/gostaticanalysis/analysisutil v0.4.1/go.mod h1:18U/DLpRgIUd459wGxVHE0fRgmo1UgHDcbw7F5idXu0= -github.com/gostaticanalysis/analysisutil v0.7.1/go.mod h1:v21E3hY37WKMGSnbsw2S/ojApNWb6C1//mXO48CXbVc= -github.com/gostaticanalysis/comment v1.3.0/go.mod h1:xMicKDx7XRXYdVwY9f9wQpDJVnqWxw9wCauCMKp+IBI= -github.com/gostaticanalysis/comment v1.4.1/go.mod h1:ih6ZxzTHLdadaiSnF5WY3dxUoXfXAlTaRzuaNDlSado= -github.com/gostaticanalysis/comment v1.4.2/go.mod h1:KLUTGDv6HOCotCH8h2erHKmpci2ZoR8VPu34YA2uzdM= -github.com/gostaticanalysis/forcetypeassert v0.0.0-20200621232751-01d4955beaa5/go.mod h1:qZEedyP/sY1lTGV1uJ3VhWZ2mqag3IkWsDHVbplHXak= -github.com/gostaticanalysis/nilerr v0.1.1/go.mod h1:wZYb6YI5YAxxq0i1+VJbY0s2YONW0HU0GPE3+5PWN4A= -github.com/gostaticanalysis/testutil v0.3.1-0.20210208050101-bfb5c8eec0e4/go.mod h1:D+FIZ+7OahH3ePw/izIEeH5I06eKs1IKI4Xr64/Am3M= -github.com/gostaticanalysis/testutil v0.4.0/go.mod h1:bLIoPefWXrRi/ssLFWX1dx7Repi5x3CuviD3dgAZaBU= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= -github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= -github.com/grpc-ecosystem/go-grpc-middleware v1.2.2/go.mod h1:EaizFBKfUKtMIF5iaDEhniwNedqGo9FuLFzppDr3uwI= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= -github.com/grpc-ecosystem/grpc-gateway v1.12.1/go.mod h1:8XEsbTttt/W+VvjtQhLACqCisSPWTxCZ7sBRjU6iH9c= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= -github.com/hashicorp/consul/api v1.10.1/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M= -github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= -github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms= github.com/hashicorp/errwrap v0.0.0-20141028054710-7554cd9344ce/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= -github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= -github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= -github.com/hashicorp/go-hclog v0.14.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= -github.com/hashicorp/go-hclog v0.16.2/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= -github.com/hashicorp/go-hclog v1.2.1/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= -github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= -github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= -github.com/hashicorp/go-kms-wrapping/entropy v0.1.0/go.mod h1:d1g9WGtAunDNpek8jUIEJnBlbgKS1N2Q61QkHiZyR1g= -github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-hclog v1.3.1 h1:vDwF1DFNZhntP4DAjuTpOw3uEgMUpXh1pB5fW9DqHpo= github.com/hashicorp/go-multierror v0.0.0-20161216184304-ed905158d874/go.mod h1:JMRHfdO9jKNzS/+BTlxCjKNQHg/jZAft8U7LloJvN7I= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= -github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= -github.com/hashicorp/go-plugin v1.4.3/go.mod h1:5fGEH17QVwTTcR0zV7yhDPLLmFX9YSZ38b18Udy6vYQ= -github.com/hashicorp/go-plugin v1.4.4/go.mod h1:viDMjcLJuDui6pXb8U4HVfb8AamCWhHGUjr2IrTF67s= -github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= -github.com/hashicorp/go-retryablehttp v0.6.6/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= -github.com/hashicorp/go-retryablehttp v0.7.1/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= -github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= -github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= -github.com/hashicorp/go-secure-stdlib/base62 v0.1.1/go.mod h1:EdWO6czbmthiwZ3/PUsDV+UD1D5IRU4ActiaWGwt0Yw= -github.com/hashicorp/go-secure-stdlib/mlock v0.1.1/go.mod h1:zq93CJChV6L9QTfGKtfBxKqD7BqqXx5O04A/ns2p5+I= -github.com/hashicorp/go-secure-stdlib/mlock v0.1.2/go.mod h1:zq93CJChV6L9QTfGKtfBxKqD7BqqXx5O04A/ns2p5+I= -github.com/hashicorp/go-secure-stdlib/parseutil v0.1.1/go.mod h1:QmrqtbKuxxSWTN3ETMPuB+VtEiBJ/A9XhoYGv8E1uD8= -github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6/go.mod h1:QmrqtbKuxxSWTN3ETMPuB+VtEiBJ/A9XhoYGv8E1uD8= -github.com/hashicorp/go-secure-stdlib/password v0.1.1/go.mod h1:9hH302QllNwu1o2TGYtSk8I8kTAN0ca1EHpwhm5Mmzo= -github.com/hashicorp/go-secure-stdlib/strutil v0.1.1/go.mod h1:gKOamz3EwoIoJq7mlMIRBpVTAUn8qPCrEclOKKWhD3U= -github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4= -github.com/hashicorp/go-secure-stdlib/tlsutil v0.1.1/go.mod h1:l8slYwnJA26yBz+ErHpp2IRCLr0vuOMGBORIz4rRiAs= -github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= -github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A= -github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= -github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= +github.com/hashicorp/go-retryablehttp v0.7.2 h1:AcYqCvkpalPnPF2pn0KamgwamS42TqUDDYFRKq/RAd0= +github.com/hashicorp/go-retryablehttp v0.7.2/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= -github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= -github.com/hashicorp/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg2DmyNY= -github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= -github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= -github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= -github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk= -github.com/hashicorp/vault/api v1.7.2/go.mod h1:xbfA+1AvxFseDzxxdWaL0uO99n1+tndus4GCrtouy0M= -github.com/hashicorp/vault/sdk v0.5.1/go.mod h1:DoGraE9kKGNcVgPmTuX357Fm6WAx1Okvde8Vp3dPDoU= -github.com/hashicorp/vault/sdk v0.5.3/go.mod h1:DoGraE9kKGNcVgPmTuX357Fm6WAx1Okvde8Vp3dPDoU= -github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= -github.com/hashicorp/yamux v0.1.0/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= -github.com/honeycombio/beeline-go v1.1.1/go.mod h1:kN0cfUGBMfA87DyCYbiiLoSzWsnw3bluZvNEWtatHxk= -github.com/honeycombio/beeline-go v1.9.0 h1:MM/20HBFE2AKno0N9bOgEicSHHiTkTklGdtjdtAEWbk= -github.com/honeycombio/beeline-go v1.9.0/go.mod h1:/8gmL2gGFbAnIhfldNUj26AFi7+JTdjtTfIujJww6yI= -github.com/honeycombio/libhoney-go v1.15.2/go.mod h1:JzhRPYgoBCd0rZvudrqmej4Ntx0w7AT3wAJpf5+t1WA= -github.com/honeycombio/libhoney-go v1.15.8 h1:TECEltZ48K6J4NG1JVYqmi0vCJNnHYooFor83fgKesA= -github.com/honeycombio/libhoney-go v1.15.8/go.mod h1:+tnL2etFnJmVx30yqmoUkVyQjp7uRJw0a2QGu48lSyY= +github.com/honeycombio/beeline-go v1.10.0 h1:cUDe555oqvw8oD76BQJ8alk7FP0JZ/M/zXpNvOEDLDc= +github.com/honeycombio/libhoney-go v1.16.0 h1:kPpqoz6vbOzgp7jC6SR7SkNj7rua7rgxvznI6M3KdHc= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/huandu/xstrings v1.0.0/go.mod h1:4qWG/gcEcfX4z/mBDHJ++3ReCw9ibxbsNJbcucJdbSo= -github.com/huandu/xstrings v1.2.0/go.mod h1:DvyZB1rfVYsBIigL8HwpZgxHwXozlTgGqn63UyNX5k4= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/imdario/mergo v0.3.4/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/imdario/mergo v0.3.10/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= @@ -857,258 +544,98 @@ github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/j-keck/arping v0.0.0-20160618110441-2cf9dc699c56/go.mod h1:ymszkNOg6tORTn+6F6j+Jc8TOr5osrynvN6ivFWZ2GA= -github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= -github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= -github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= -github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= -github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= -github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= -github.com/jackc/pgconn v1.5.0/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI= -github.com/jackc/pgconn v1.6.0/go.mod h1:yeseQo4xhQbgyJs2c87RAXOH2i624N0Fh1KSPJya7qo= -github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= -github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY= -github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= -github.com/jackc/pgconn v1.10.0/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= -github.com/jackc/pgconn v1.10.1/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= -github.com/jackc/pgconn v1.12.1/go.mod h1:ZkhRC59Llhrq3oSfrikvwQ5NaxYExr6twkdkMLaKono= -github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= -github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= -github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c= -github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak= -github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= -github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= -github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= -github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= -github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= -github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= -github.com/jackc/pgproto3/v2 v2.0.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= -github.com/jackc/pgproto3/v2 v2.0.2/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= -github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= -github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= -github.com/jackc/pgproto3/v2 v2.3.0/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= -github.com/jackc/pgservicefile v0.0.0-20200307190119-3430c5407db8/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= -github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= -github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= -github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= -github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= -github.com/jackc/pgtype v1.3.0/go.mod h1:b0JqxHvPmljG+HQ5IsvQ0yqeSi4nGcDTVjFoiLDb0Ik= -github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM= -github.com/jackc/pgtype v1.8.1/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= -github.com/jackc/pgtype v1.11.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= -github.com/jackc/pgx v3.6.2+incompatible/go.mod h1:0ZGrqGqkRlliWnWB4zKnWtjbSWbGkVEFm4TeybAXq+I= -github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= -github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= -github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= -github.com/jackc/pgx/v4 v4.6.0/go.mod h1:vPh43ZzxijXUVJ+t/EmXBtFmbFVO72cuneCT9oAlxAg= -github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= -github.com/jackc/pgx/v4 v4.13.0/go.mod h1:9P4X524sErlaxj0XSGZk7s+LD0eOyu1ZDUrrpznYDF0= -github.com/jackc/pgx/v4 v4.16.1/go.mod h1:SIhx0D5hoADaiXZVyv+3gSm3LCIIINTVO0PficsvWGQ= -github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jackc/puddle v1.1.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jackc/puddle v1.2.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jellydator/ttlcache/v2 v2.11.1/go.mod h1:RtE5Snf0/57e+2cLWFYWCCsLas2Hy3c5Z4n14XmSvTI= -github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= -github.com/jgautheron/goconst v1.5.1/go.mod h1:aAosetZ5zaeC/2EfMeRswtxUFBpe2Hr7HzkgX4fanO4= -github.com/jhump/gopoet v0.0.0-20190322174617-17282ff210b3/go.mod h1:me9yfT6IJSlOL3FCfrg+L6yzUEZ+5jW6WHt4Sk+UPUI= -github.com/jhump/gopoet v0.1.0/go.mod h1:me9yfT6IJSlOL3FCfrg+L6yzUEZ+5jW6WHt4Sk+UPUI= -github.com/jhump/goprotoc v0.5.0/go.mod h1:VrbvcYrQOrTi3i0Vf+m+oqQWk9l72mjkJCYo7UvLHRQ= -github.com/jhump/protoreflect v1.6.0/go.mod h1:eaTn3RZAmMBcV0fifFvlm6VHNz3wSkYyXYWUh7ymB74= -github.com/jhump/protoreflect v1.6.1/go.mod h1:RZQ/lnuN+zqeRVpQigTwO6o0AJUkxbnSnpuG7toUTG4= -github.com/jhump/protoreflect v1.11.0/go.mod h1:U7aMIjN0NWq9swDP7xDdoMfRHb35uiuTd3Z9nFXJf5E= -github.com/jhump/protoreflect v1.12.0/go.mod h1:JytZfP5d0r8pVNLZvai7U/MCuTWITgrI4tTg7puQFKI= -github.com/jingyugao/rowserrcheck v1.1.1/go.mod h1:4yvlZSDb3IyDTUZJUmpZfm2Hwok+Dtp+nu2qOq+er9c= -github.com/jirfag/go-printf-func-name v0.0.0-20200119135958-7558a9eaa5af/go.mod h1:HEWGJkRDzjJY2sqdDwxccsGicWEf9BQOZsq2tV+xzM0= github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jmespath/go-jmespath v0.0.0-20160803190731-bd40a432e4c7/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= -github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= -github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= -github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/jmhodges/clock v0.0.0-20160418191101-880ee4c33548 h1:dYTbLf4m0a5u0KLmPfB6mgxbcV7588bOCx79hxa5Sr4= -github.com/jmhodges/clock v0.0.0-20160418191101-880ee4c33548/go.mod h1:hGT6jSUVzF6no3QaDSMLGLEHtHSBSefs+MgcDWnmhmo= -github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks= -github.com/jmoiron/sqlx v1.3.4/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ= -github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= github.com/joefitzgerald/rainbow-reporter v0.1.0/go.mod h1:481CNgqmVHQZzdIbN52CupLJyoVwB10FQ/IQlF1pdL8= github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= -github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= -github.com/jonboulle/clockwork v0.2.0/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= -github.com/josharian/txtarfs v0.0.0-20210218200122-0702f000015a/go.mod h1:izVPOvVRsHiKkeGCT6tYBNWyDVuzj9wAaBb5R9qamfw= -github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= -github.com/juju/ratelimit v1.0.1/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= -github.com/julz/importas v0.0.0-20210419104244-841f0c0fe66d/go.mod h1:oSFU2R4XK/P7kNBrnL/FEQlDGN1/6WoxXEjSSXO0DV0= -github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k= -github.com/karrick/godirwalk v1.15.3/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk= -github.com/karrick/godirwalk v1.16.1/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk= -github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= +github.com/karrick/godirwalk v1.8.0/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaRPx4tDPEn4= +github.com/karrick/godirwalk v1.10.3/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= -github.com/kisielk/errcheck v1.6.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.11.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= -github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.11.13/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= -github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= -github.com/klauspost/compress v1.13.5/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= -github.com/klauspost/compress v1.15.7/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= -github.com/klauspost/compress v1.15.8/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= -github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= -github.com/klauspost/compress v1.15.10 h1:Ai8UzuomSCDw90e1qNMtb15msBXsNpH6gzkkENQNcJo= -github.com/klauspost/compress v1.15.10/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM= -github.com/klauspost/pgzip v1.2.5 h1:qnWYvvKqedOF2ulHpMG72XQol4ILEJ8k2wwRl/Km8oE= -github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= +github.com/klauspost/compress v1.15.12/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM= +github.com/klauspost/compress v1.15.15 h1:EF27CXIuDsYJ6mmvtBRlEuB2UVOqHG1tAXgZ7yIO+lw= +github.com/klauspost/compress v1.15.15/go.mod h1:ZcK2JAFqKOpnBlxcLsJzYfrS9X1akm9fHZNnD9+Vo/4= +github.com/klauspost/pgzip v1.2.6-0.20220930104621-17e8dac29df8 h1:BcxbplxjtczA1a6d3wYoa7a0WL3rq9DKBMGHeKyjEF0= +github.com/klauspost/pgzip v1.2.6-0.20220930104621-17e8dac29df8/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= -github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/kulti/thelper v0.4.0/go.mod h1:vMu2Cizjy/grP+jmsvOFDx1kYP6+PD1lqg4Yu5exl2U= -github.com/kunwardeep/paralleltest v1.0.3/go.mod h1:vLydzomDFpk7yu5UX02RmP0H8QfRPOV/oFhWN85Mjb4= -github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -github.com/kyoh86/exportloopref v0.1.8/go.mod h1:1tUcJeiioIs7VWe5gcOObrux3lb66+sBqGZrRkMwPgg= -github.com/labstack/echo/v4 v4.3.0/go.mod h1:PvmtTvhVqKDzDQy4d3bWzPjZLzom4iQbAZy2sgZ/qI8= -github.com/labstack/echo/v4 v4.7.2/go.mod h1:xkCDAdFCIf8jsFQ5NnbK7oqaF/yU1A1X20Ltm0OvSks= -github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= -github.com/labstack/gommon v0.3.1/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM= -github.com/ldez/gomoddirectives v0.2.2/go.mod h1:cpgBogWITnCfRq2qGoDkKMEVSaarhdBr6g8G04uz6d0= -github.com/ldez/tagliatelle v0.2.0/go.mod h1:8s6WJQwEYHbKZDsp/LjArytKOG8qaMrKQQ3mFukHs88= -github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= -github.com/letsencrypt/boulder v0.0.0-20220723181115-27de4befb95e h1:2ba+yBBeT8ZFyZjRLPDKvkqVrWX4CCYAuR6nuJGojD0= -github.com/letsencrypt/boulder v0.0.0-20220723181115-27de4befb95e/go.mod h1:54WQpg5QI0mpRhxoj9bxysLqA5WJylVsLtXOrb3zAiU= -github.com/letsencrypt/challtestsrv v1.2.1/go.mod h1:Ur4e4FvELUXLGhkMztHOsPIsvGxD/kzSJninOrkM+zc= -github.com/letsencrypt/pkcs11key/v4 v4.0.0/go.mod h1:EFUvBDay26dErnNb70Nd0/VW3tJiIbETBPTl9ATXQag= -github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/lib/pq v1.9.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/lib/pq v1.10.3/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/lib/pq v1.10.4/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/lib/pq v1.10.6/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= +github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= +github.com/letsencrypt/boulder v0.0.0-20230213213521-fdfea0d469b6 h1:unJdfS94Y3k85TKy+mvKzjW5R9rIC+Lv4KGbE7uNu0I= +github.com/letsencrypt/boulder v0.0.0-20230213213521-fdfea0d469b6/go.mod h1:PUgW5vI9ANEaV6qv9a6EKu8gAySgwf0xrzG9xIB/CK0= github.com/linuxkit/virtsock v0.0.0-20201010232012-f8cee7dfc7a3/go.mod h1:3r6x7q95whyfWQpmGZTu3gk3v2YkMi05HEzl7Tf7YEo= -github.com/logrusorgru/aurora v0.0.0-20181002194514-a7b3b318ed4e/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= -github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= -github.com/luna-duclos/instrumentedsql v1.1.3/go.mod h1:9J1njvFds+zN7y85EDhN9XNQLANWwZt2ULeIC8yMNYs= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= -github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= -github.com/maratori/testpackage v1.0.1/go.mod h1:ddKdw+XG0Phzhx8BFDTKgpWP4i7MpApTE5fXSKAqwDU= -github.com/markbates/errx v1.1.0/go.mod h1:PLa46Oex9KNbVDZhKel8v1OT7hD5JZ2eI7AHhA0wswc= -github.com/markbates/oncer v1.0.0/go.mod h1:Z59JA581E9GP6w96jai+TGqafHPW+cPfRxz2aSZ0mcI= +github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE= github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0= github.com/marstr/guid v1.1.0/go.mod h1:74gB1z2wpxxInTG6yaqA7KrtM0NZ+RbrcqDvYHefzho= -github.com/matoous/godox v0.0.0-20210227103229-6504466cf951/go.mod h1:1BELzlh859Sh1c6+90blK8lbYy0kwQf1bYlBhBysy1s= -github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= -github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= -github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= -github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= -github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= -github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= -github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= -github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= -github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= -github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= -github.com/mattn/go-runewidth v0.0.6/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= -github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= +github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= github.com/mattn/go-shellwords v1.0.6/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk= github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= -github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= -github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= -github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= -github.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= -github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= -github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI= github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= github.com/maxbrunsfeld/counterfeiter/v6 v6.2.2/go.mod h1:eD9eIE7cdwcMi9rYluz88Jz2VyhSmden33/aXg4oVIY= -github.com/mbilski/exhaustivestruct v1.2.0/go.mod h1:OeTBVxQWoEmB2J2JCHmXWPJ0aksxSUOUy+nvtVEfzXc= -github.com/mgechev/dots v0.0.0-20210922191527-e955255bf517/go.mod h1:KQ7+USdGKfpPjXk4Ga+5XxQM4Lm4e3gAogrreFAYpOg= -github.com/mgechev/revive v1.1.2/go.mod h1:bnXsMr+ZTH09V5rssEI+jHAZ4z+ZdyhgO/zsy3EhK+0= -github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc= -github.com/microcosm-cc/bluemonday v1.0.16/go.mod h1:Z0r70sCuXHig8YpBzCc5eGHAap2K7e/u082ZUpDRRqM= -github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= -github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= -github.com/miekg/dns v1.1.35/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM= -github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4= -github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= -github.com/miekg/pkcs11 v1.0.2/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= github.com/miekg/pkcs11 v1.1.1 h1:Ugu9pdy6vAYku5DEpVWVFPYnzV+bxB+iRdbuFSu7TvU= github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= -github.com/mistifyio/go-zfs v2.1.2-0.20190413222219-f784269be439+incompatible h1:aKW/4cBs+yK6gpqU3K/oIwk9Q/XICqd3zOX/UFuvqmk= github.com/mistifyio/go-zfs v2.1.2-0.20190413222219-f784269be439+incompatible/go.mod h1:8AuVvqP/mXw1px98n46wfvcGfQ4ci2FwoAjKYxuo3Z4= -github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= -github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= -github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= -github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= -github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mistifyio/go-zfs/v3 v3.0.0 h1:J5QK618xRcXnQYZ2GE5FdmpS1ufIrWue+lR/mpe6/14= +github.com/mistifyio/go-zfs/v3 v3.0.0/go.mod h1:CzVgeB0RvF2EGzQnytKVvVSDwmKJXxkOTUGbNrTja/k= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg= -github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= -github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= -github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= -github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= -github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= -github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= -github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/mitchellh/mapstructure v1.4.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f/go.mod h1:OkQIRizQZAeMln+1tSwduZz7+Af5oFlKirV/MSYes2A= -github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= -github.com/mitchellh/reflectwalk v1.0.1/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= -github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc= github.com/moby/sys/mountinfo v0.4.0/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A= github.com/moby/sys/mountinfo v0.4.1/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A= @@ -1117,8 +644,8 @@ github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vyg github.com/moby/sys/mountinfo v0.6.2/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI= github.com/moby/sys/symlink v0.1.0/go.mod h1:GGDODQmbFOjFsXvfLVn3+ZRxkch54RkSiGqsZeMYowQ= github.com/moby/term v0.0.0-20200312100748-672ec06f55cd/go.mod h1:DdlQx2hp0Ss5/fLikoLlEeIYiATotOjgB//nb973jeo= -github.com/moby/term v0.0.0-20210610120745-9d4ed1856297 h1:yH0SvLzcbZxcJXho2yh7CqdENGMQe73Cw3woZBpPli0= -github.com/moby/term v0.0.0-20210610120745-9d4ed1856297/go.mod h1:vgPCkQMyxTZ7IDy8SXRufE172gr8+K/JE/7hHFxHW3A= +github.com/moby/term v0.0.0-20221120202655-abb19827d345 h1:J9c53/kxIH+2nTKBEfZYFMlhghtHpIHSXpm5VRGHSnU= +github.com/moby/term v0.0.0-20221120202655-abb19827d345/go.mod h1:15ce4BGCFxt7I5NQKT+HV0yEDxmf6fSysfEDiVo3zFM= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -1126,65 +653,36 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= -github.com/moricho/tparallel v0.2.1/go.mod h1:fXEIZxG2vdfl0ZF8b42f5a78EhjjD5mX8qUplsoSU4k= +github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= -github.com/mozilla/scribe v0.0.0-20180711195314-fb71baf557c1/go.mod h1:FIczTrinKo8VaLxe6PWTPEXRXDIHz2QAwiaBaP5/4a8= -github.com/mozilla/tls-observatory v0.0.0-20210609171429-7bc42856d2e5/go.mod h1:FUqVoUPHSEdDR0MnFM3Dh8AU0pZHLXUD127SAJGER/s= -github.com/mreiferson/go-httpclient v0.0.0-20160630210159-31f0106b4474/go.mod h1:OQA4XLvDbMgS8P0CevmM4m9Q3Jq4phKUzcocxuGJ5m8= github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ= github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/mwitkow/go-proto-validators v0.0.0-20180403085117-0950a7990007/go.mod h1:m2XC9Qq0AlmmVksL6FktJCdTYyLk7V3fKyp0sl1yWQo= -github.com/mwitkow/go-proto-validators v0.2.0/go.mod h1:ZfA1hW+UH/2ZHOWvQ3HnQaU0DtnpXu850MZiy+YUgcc= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= -github.com/nakabonne/nestif v0.3.1/go.mod h1:9EtoZochLn5iUprVDmDjqGKPofoUEBL8U4Ngq6aY7OE= -github.com/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354/go.mod h1:KSVJerMDfblTH7p5MZaTt+8zaT2iEk3AkVb9PQdZuE8= github.com/ncw/swift v1.0.47/go.mod h1:23YIA4yWVnGwv2dQlN4bB7egfYX6YLn0Yo/S6zZO/ZM= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= -github.com/nishanths/exhaustive v0.2.3/go.mod h1:bhIX678Nx8inLM9PbpvK1yv6oGtoP8BfaIeMzgBNKvc= -github.com/nishanths/predeclared v0.0.0-20190419143655-18a43bb90ffc/go.mod h1:62PewwiQTlm/7Rj+cxVYqZvDIUc+JjZq6GHAC1fsObQ= -github.com/nishanths/predeclared v0.2.1/go.mod h1:HvkGJcA3naj4lOwnFXFDkFxVtSqQMB9sbB1usJ+xjQE= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= -github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= -github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= -github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= +github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= -github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= -github.com/olekukonko/tablewriter v0.0.2/go.mod h1:rSAaSIOAGT9odnlyGlUfAJaoc5w2fSBUmeGDbRWPxyQ= -github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/onsi/ginkgo v0.0.0-20151202141238-7f8ab55aaf3b/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= -github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= -github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= -github.com/onsi/ginkgo/v2 v2.0.0/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= -github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= github.com/onsi/gomega v0.0.0-20151007035656-2152b45fa28a/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= -github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA= -github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc= -github.com/onsi/gomega v1.16.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= -github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= -github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs= -github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= -github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= @@ -1194,16 +692,16 @@ github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3I github.com/opencontainers/image-spec v1.0.0/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= -github.com/opencontainers/image-spec v1.0.3-0.20220114050600-8b9d41f48198 h1:+czc/J8SlhPKLOtVLMQc+xDCFBT73ZStMsRhSsUhsSg= -github.com/opencontainers/image-spec v1.0.3-0.20220114050600-8b9d41f48198/go.mod h1:j4h1pJW6ZcJTgMZWP3+7RlG3zTaP02aDZ/Qw0sppK7Q= +github.com/opencontainers/image-spec v1.1.0-rc2 h1:2zx/Stx4Wc5pIPDvIxHXvXtQFW/7XWJGmnM7r3wg034= +github.com/opencontainers/image-spec v1.1.0-rc2/go.mod h1:3OVijpioIKYWTqjiG0zfF6wvoJ4fAXGbjdZuI2NgsRQ= github.com/opencontainers/runc v0.0.0-20190115041553-12f6a991201f/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= github.com/opencontainers/runc v1.0.0-rc8.0.20190926000215-3e425f80a8c9/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= github.com/opencontainers/runc v1.0.0-rc9/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= github.com/opencontainers/runc v1.0.0-rc93/go.mod h1:3NOsor4w32B2tC0Zbl8Knk4Wg84SM2ImC1fxBuqJ/H0= github.com/opencontainers/runc v1.0.2/go.mod h1:aTaHFFwQXuA71CiyxOdFFIorAoemI04suvGRQFzWTD0= -github.com/opencontainers/runc v1.1.3 h1:vIXrkId+0/J2Ymu2m7VjGvbSlAId9XNRPhn2p4b+d8w= -github.com/opencontainers/runc v1.1.3/go.mod h1:1J5XiS+vdZ3wCyZybsuxXZWGrgSr8fFJHLXuG2PsnNg= +github.com/opencontainers/runc v1.1.4 h1:nRCz/8sKg6K6jgYAFLDlXzPeITBZJyX28DBVhWD+5dg= +github.com/opencontainers/runc v1.1.4/go.mod h1:1J5XiS+vdZ3wCyZybsuxXZWGrgSr8fFJHLXuG2PsnNg= github.com/opencontainers/runtime-spec v0.1.2-0.20190507144316-5b71a03e2700/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= github.com/opencontainers/runtime-spec v1.0.1/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= github.com/opencontainers/runtime-spec v1.0.2-0.20190207185410-29686dbc5559/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= @@ -1216,42 +714,24 @@ github.com/opencontainers/selinux v1.6.0/go.mod h1:VVGKuOLlE7v4PJyT6h7mNWvq1rzqi github.com/opencontainers/selinux v1.8.0/go.mod h1:RScLhm78qiWa2gbVCcGkC7tCGdgk3ogry1nUQF8Evvo= github.com/opencontainers/selinux v1.8.2/go.mod h1:MUIHuUEvKB1wtJjQdOyYRgOnLD2xAPP8dBsCoU0KuF8= github.com/opencontainers/selinux v1.10.0/go.mod h1:2i0OySw99QjzBBQByd1Gr9gSjvuho1lHsJxIJ3gGbJI= -github.com/opencontainers/selinux v1.10.1 h1:09LIPVRP3uuZGQvgR+SgMSNBd1Eb3vlRbGqQpoHsF8w= -github.com/opencontainers/selinux v1.10.1/go.mod h1:2i0OySw99QjzBBQByd1Gr9gSjvuho1lHsJxIJ3gGbJI= -github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/opencontainers/selinux v1.11.0 h1:+5Zbo97w3Lbmb3PeqQtpmTkMwsW5nRI3YaLpt7tQ7oU= +github.com/opencontainers/selinux v1.11.0/go.mod h1:E5dMC3VPuVvVHDYmi78qvhJp8+M586T4DlDRYpFkyec= +github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= +github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= github.com/ostreedev/ostree-go v0.0.0-20210805093236-719684c64e4f h1:/UDgs8FGMqwnHagNDPGOlts35QkhAZ8by3DR7nMih7M= github.com/ostreedev/ostree-go v0.0.0-20210805093236-719684c64e4f/go.mod h1:J6OG6YJVEWopen4avK3VNQSnALmmjvniMmni/YFYAwc= -github.com/otiai10/copy v1.2.0/go.mod h1:rrF5dJ5F0t/EWSYODDu4j9/vEeYHMkc8jt0zJChqQWw= -github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE= -github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs= -github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo= -github.com/otiai10/mint v1.3.1/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc= -github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE= github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc= -github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= -github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= -github.com/phayes/checkstyle v0.0.0-20170904204023-bfd46e6a821d/go.mod h1:3OzsM7FXDQlpCiw2j81fOmAwQLnZnLGXVKUzeKQXIAw= -github.com/pierrec/lz4 v2.5.2+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= -github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= -github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1-0.20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= -github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/polyfloyd/go-errorlint v0.0.0-20210722154253-910bb7978349/go.mod h1:wi9BfjxjF/bwiZ701TzmfKu6UKC357IOAtNr0Td0Lvw= -github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= -github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= -github.com/poy/onpar v0.0.0-20190519213022-ee068f8ea4d1/go.mod h1:nSbFQvMj97ZyhFRSJYtut+msi4sOY6zJDGCdSc+/rZU= github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA= github.com/proglottis/gpgme v0.1.3 h1:Crxx0oz4LKB3QXc5Ea0J19K/3ICfy3ftr5exgUK1AU0= github.com/proglottis/gpgme v0.1.3/go.mod h1:fPbW/EZ0LvwQtH8Hy7eixhp1eF3G39dtx7GUN+0Gmy0= @@ -1260,29 +740,21 @@ github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXP github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= -github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= -github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= -github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= -github.com/prometheus/client_golang v1.12.2 h1:51L9cDoUHVrXx4zWYlcLQIZ+d+VXHgqnYKkIuq4g/34= -github.com/prometheus/client_golang v1.12.2/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= +github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw= github.com/prometheus/client_model v0.0.0-20171117100541-99fa1f4be8e5/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= github.com/prometheus/common v0.0.0-20180110214958-89604d197083/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= -github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= -github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= -github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= -github.com/prometheus/common v0.37.0 h1:ccBbHCgIiT9uSoFY0vX8H3zsNR5eLt17/RQLUvn8pXE= -github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA= +github.com/prometheus/common v0.39.0 h1:oOyhkDq05hPZKItWVBkJ6g6AtGxi+fy7F4JvUV8uhsI= github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= @@ -1294,71 +766,40 @@ github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+Gx github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU= -github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= -github.com/pseudomuto/protoc-gen-doc v1.3.2/go.mod h1:y5+P6n3iGrbKG+9O04V5ld71in3v/bX88wUwgt+U8EA= -github.com/pseudomuto/protokit v0.2.0/go.mod h1:2PdH30hxVHsup8KpBTOXTBeMVhJZVio3Q8ViKSAXT0Q= -github.com/quasilyte/go-consistent v0.0.0-20190521200055-c6f3937de18c/go.mod h1:5STLWrekHfjyYwxBRVRXNOSewLJ3PWfDJd1VyTS21fI= -github.com/quasilyte/go-ruleguard v0.3.1-0.20210203134552-1b5a410e1cc8/go.mod h1:KsAh3x0e7Fkpgs+Q9pNLS5XpFSvYCEVl5gP9Pp1xp30= -github.com/quasilyte/go-ruleguard v0.3.13/go.mod h1:Ul8wwdqR6kBVOCt2dipDBkE+T6vAV/iixkrKuRTN1oQ= -github.com/quasilyte/go-ruleguard/dsl v0.3.0/go.mod h1:KeCP03KrjuSO0H1kTuZQCWlQPulDV6YMIXmpQss17rU= -github.com/quasilyte/go-ruleguard/dsl v0.3.10/go.mod h1:KeCP03KrjuSO0H1kTuZQCWlQPulDV6YMIXmpQss17rU= -github.com/quasilyte/go-ruleguard/rules v0.0.0-20201231183845-9e62ed36efe1/go.mod h1:7JTjp89EGyU1d6XfBiXihJNG37wB2VRkd125Q1u7Plc= -github.com/quasilyte/go-ruleguard/rules v0.0.0-20210428214800-545e0d2e0bf7/go.mod h1:4cgAphtvu7Ftv7vOT2ZOYhC6CvBxZixcasr8qIOTA50= -github.com/quasilyte/regex/syntax v0.0.0-20200407221936-30656e2c4a95/go.mod h1:rlzQ04UMyJXu/aOvhd8qT+hvDrFpiwqp8MRXDY9szc0= -github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.3 h1:utMvzDsuh3suAEnhH0RdHmoPbU648o6CvXxTx4SBMOw= +github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.3.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/rogpeppe/go-internal v1.5.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/rogpeppe/go-internal v1.6.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= -github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= -github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= -github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= -github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= -github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= -github.com/russross/blackfriday v1.6.0/go.mod h1:ti0ldHuxg49ri4ksnFxlkCfN+hvslNlmVHqNRXXJNAY= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/ryancurrah/gomodguard v1.2.3/go.mod h1:rYbA/4Tg5c54mV1sv4sQTP5WOPBcoLtnBZ7/TEhXAbg= -github.com/ryanrolds/sqlclosecheck v0.3.0/go.mod h1:1gREqxyTGR3lVtpngyFo3hZAgk0KCtEdgEkHwDbigdA= -github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= -github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= -github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= github.com/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4= -github.com/sagikazarmark/crypt v0.1.0/go.mod h1:B/mN0msZuINBtQ1zZLEQcegFJJf9vnYIR88KRMEuODE= -github.com/sanposhiho/wastedassign/v2 v2.0.6/go.mod h1:KyZ0MWTwxxBmfwn33zh3k1dmsbF2ud9pAAGfoLfjhtI= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/sclevine/spec v1.2.0/go.mod h1:W4J29eT/Kzv7/b9IWLB055Z+qvVC9vt0Arko24q7p+U= -github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/sebdah/goldie/v2 v2.5.3 h1:9ES/mNN+HNUbNWpVAlrzuZ7jE+Nrczbj8uFRjM7624Y= -github.com/sebdah/goldie/v2 v2.5.3/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI= github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo= github.com/seccomp/libseccomp-golang v0.9.2-0.20220502022130-f33da4d89646/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg= -github.com/secure-systems-lab/go-securesystemslib v0.4.0/go.mod h1:FGBZgq2tXWICsxWQW1msNf49F0Pf2Op5Htayx335Qbs= -github.com/securego/gosec/v2 v2.9.1/go.mod h1:oDcDLcatOJxkCGaCaq8lua1jTnYf6Sou4wdiJ1n4iHc= +github.com/segmentio/ksuid v1.0.4 h1:sBo2BdShXjmcugAMwjugoGUdUV0pcxY5mW4xKRn3v4c= github.com/segmentio/ksuid v1.0.4/go.mod h1:/XUiZBD3kVx5SmUOl55voK5yeAbBNNIed+2O73XgrPE= -github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= -github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= -github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= -github.com/shazow/go-diff v0.0.0-20160112020656-b6b7b6733b8c/go.mod h1:/PevMnwAxekIXwN8qQyfc5gl2NlkB3CQlkizAbOkeBs= -github.com/shirou/gopsutil/v3 v3.21.10/go.mod h1:t75NhzCZ/dYyPQjyQmrAYP6c8+LCdFANeBMdLPCNnew= -github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= -github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= -github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= -github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= -github.com/sigstore/sigstore v1.4.1 h1:e/tfXseQRymIjgiykskciGrp75AZVCfYokZ2r9tg5vw= -github.com/sigstore/sigstore v1.4.1/go.mod h1:4+s4d6oTDdoQkf5lwpZBoOlWWV+hXhur1my9WdN5PjU= +github.com/sigstore/fulcio v1.1.0 h1:mzzJ05Ccu8Y2inyioklNvc8MpzlGHxu8YqNeTm0dHfU= +github.com/sigstore/fulcio v1.1.0/go.mod h1:zv1ZQTXZbUwQdRwajlQksc34pRas+2aZYpIZoQBNev8= +github.com/sigstore/rekor v1.0.1 h1:rcESXSNkAPRWFYZel9rarspdvneET60F2ngNkadi89c= +github.com/sigstore/rekor v1.0.1/go.mod h1:ecTKdZWGWqE1pl3U1m1JebQJLU/hSjD9vYHOmHQ7w4g= +github.com/sigstore/sigstore v1.5.1 h1:iUou0QJW8eQKMUkTXbFyof9ZOblDtfaW2Sn2+QI8Tcs= +github.com/sigstore/sigstore v1.5.1/go.mod h1:3i6UTWVNtFwOtbgG63FZZNID4vO9KcO8AszIJlaNI8k= github.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= @@ -1366,43 +807,25 @@ github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/sivchari/tenv v1.4.7/go.mod h1:5nF+bITvkebQVanjU6IuMbvIot/7ReNsUV7I5NbprB0= +github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA= github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= -github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= -github.com/sonatard/noctx v0.0.1/go.mod h1:9D2D/EoULe8Yy2joDHJj7bv3sZoq9AaSb8B4lqBjiZI= -github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE= -github.com/sourcegraph/go-diff v0.6.1/go.mod h1:iBszgVvyxdc8SFZ7gm69go2KDdt3ag071iBaWPF6cjs= -github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= -github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= -github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= -github.com/spf13/cobra v0.0.6/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= -github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk= -github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g= -github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= -github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.1-0.20171106142849-4c012f6dcd95/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= -github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns= -github.com/spf13/viper v1.9.0/go.mod h1:+i6ajR7OX2XaiBkrcZJFK21htRk7eDeLg7+O6bhUPP4= -github.com/ssgreg/nlreturn/v2 v2.2.1/go.mod h1:E/iiPB78hV7Szg2YfRgyIrk1AD6JVMTRkkxBiELzh2I= github.com/stefanberger/go-pkcs11uri v0.0.0-20201008174630-78d3cae3a980 h1:lIOOHPEbXzO3vnmx2gok1Tfs31Q8GQqKLc8vVqyQq/I= github.com/stefanberger/go-pkcs11uri v0.0.0-20201008174630-78d3cae3a980/go.mod h1:AO3tvPzVZ/ayst6UlUKUv6rcPQInYe3IknH3jYhAKu8= github.com/stretchr/objx v0.0.0-20180129172003-8a3f7159479f/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -1410,9 +833,8 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/testify v0.0.0-20170130113145-4d4bfba8f1d1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v0.0.0-20180303142811-b89eecf5ca5d/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.1.4/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -1420,82 +842,53 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= -github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= -github.com/sylabs/sif/v2 v2.7.2 h1:eCxtl2ub9fPfrO7g2JPagn6HKDhv+Kl92Jz6+ww2Y1Q= -github.com/sylabs/sif/v2 v2.7.2/go.mod h1:LQOdYXC9a8i7BleTKRw9lohi0rTbXkJOeS9u0ebvgyM= -github.com/sylvia7788/contextcheck v1.0.4/go.mod h1:vuPKJMQ7MQ91ZTqfdyreNKwZjyUg6KO+IebVyQDedZQ= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/sylabs/sif/v2 v2.9.1 h1:LxF9EcH4hmwSqDBdRv9Tt57YVkvV9rDu66AA/nmns2Y= +github.com/sylabs/sif/v2 v2.9.1/go.mod h1:10lbqUw/uptKH4Z6dRDZl+9Iz7jMiFMDE99eHRJDwOs= github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 h1:kdXcSzyDtseVEc4yCz2qF8ZrQvIDBJLl4S1c3GCXmoI= github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= -github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= -github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d/go.mod h1:RRCYJbIwD5jmqPI9XoAFR0OcDxqUctll6zUj/+B4S48= github.com/tchap/go-patricia v2.2.6+incompatible/go.mod h1:bmLyhP68RS6kStMGxByiQ23RP/odRBOTVjwp2cDyi6I= github.com/tchap/go-patricia v2.3.0+incompatible h1:GkY4dP3cEfEASBPPkWd+AmjYxhmDkqO9/zg7R0lSQRs= github.com/tchap/go-patricia v2.3.0+incompatible/go.mod h1:bmLyhP68RS6kStMGxByiQ23RP/odRBOTVjwp2cDyi6I= -github.com/tdakkota/asciicheck v0.0.0-20200416200610-e657995f937b/go.mod h1:yHp0ai0Z9gUljN3o0xMhYJnH/IcvkdTBOX2fmJ93JEM= -github.com/tenntenn/modver v1.0.1/go.mod h1:bePIyQPb7UeioSRkw3Q0XeMhYZSMx9B8ePqg6SAMGH0= -github.com/tenntenn/text/transform v0.0.0-20200319021203-7eef512accb3/go.mod h1:ON8b8w4BN/kE1EOhwT0o+d62W65a6aPw1nouo9LMgyY= -github.com/tetafro/godot v1.4.11/go.mod h1:LR3CJpxDVGlYOWn3ZZg1PgNZdTUvzsZWu8xaEohUpn8= -github.com/theupdateframework/go-tuf v0.5.0 h1:aQ7i9CBw4q9QEZifCaW6G8qGQwoN23XGaZkOA+F50z4= -github.com/theupdateframework/go-tuf v0.5.0/go.mod h1:vAqWV3zEs89byeFsAYoh/Q14vJTgJkHwnnRCWBBBINY= -github.com/timakin/bodyclose v0.0.0-20200424151742-cb6215831a94/go.mod h1:Qimiffbc6q9tBWlVV6x0P9sat/ao1xEkREYPPj9hphk= +github.com/theupdateframework/go-tuf v0.5.2 h1:habfDzTmpbzBLIFGWa2ZpVhYvFBoK0C1onC3a4zuPRA= +github.com/theupdateframework/go-tuf v0.5.2/go.mod h1:SyMV5kg5n4uEclsyxXJZI2UxPFJNDc4Y+r7wv+MlvTA= +github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 h1:e/5i7d4oYZ+C1wj2THlRK+oAhjeS/TRQwMfkIuet3w0= github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399/go.mod h1:LdwHTNJT99C5fTAzDz0ud328OgXz+gierycbcIx2fRs= -github.com/tklauser/go-sysconf v0.3.9/go.mod h1:11DU/5sG7UexIrp/O6g35hrWzu0JxlwQ3LSFUzyeuhs= -github.com/tklauser/numcpus v0.3.0/go.mod h1:yFGUr7TUHQRAhyqBcEg0Ge34zDBAsIvJJcyE6boqnA8= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/tmc/grpc-websocket-proxy v0.0.0-20200427203606-3cfed13b9966/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/tomarrell/wrapcheck/v2 v2.4.0/go.mod h1:68bQ/eJg55BROaRTbMjC7vuhL2OgfoG8bLp9ZyoBfyY= -github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce/go.mod h1:o8v6yHRoik09Xen7gje4m9ERNah1d1PPsVq1VEx9vE4= -github.com/tommy-muehle/go-mnd/v2 v2.4.0/go.mod h1:WsUAkMJMYww6l/ufffCD3m+P7LEvr8TnZn9lwVDlgzw= -github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= -github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= -github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= -github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= -github.com/ulikunitz/xz v0.5.10 h1:t92gobL9l3HE202wg3rlk19F6X+JOxl9BBrCCMYEYd8= -github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= -github.com/ultraware/funlen v0.0.3/go.mod h1:Dp4UiAus7Wdb9KUZsYWZEWiRzGuM2kXM1lPbfaF6xhA= -github.com/ultraware/whitespace v0.0.4/go.mod h1:aVMh/gQve5Maj9hQ/hg+F75lr/X5A89uZnzAmWSineA= +github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8= +github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/urfave/cli v0.0.0-20171014202726-7bc6a0acffa5/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/cli v1.22.4/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= -github.com/uudashr/gocognit v1.0.5/go.mod h1:wgYz0mitoKOTysqxTDMOUXg+Jb5SvtihkfmugIZYpEA= -github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasthttp v1.30.0/go.mod h1:2rsYD01CKFrjjsvFxx75KlEUNpWNBY9JWD3K/7o2Cus= -github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= -github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= -github.com/valyala/quicktemplate v1.7.0/go.mod h1:sqKJnoaOF88V07vkO+9FL8fb9uZg/VPSJnLYn+LmLk8= -github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= github.com/vbatts/tar-split v0.11.2 h1:Via6XqJr0hceW4wff3QRzD5gAk/tatMw/4ZA7cTlIME= github.com/vbatts/tar-split v0.11.2/go.mod h1:vV3ZuO2yWSVsz+pfFzDG/upWH1JhjOiEaWq6kXyQ3VI= -github.com/vbauerster/mpb/v7 v7.5.3 h1:BkGfmb6nMrrBQDFECR/Q7RkKCw7ylMetCb4079CGs4w= -github.com/vbauerster/mpb/v7 v7.5.3/go.mod h1:i+h4QY6lmLvBNK2ah1fSreiw3ajskRlBp9AhY/PnuOE= -github.com/viki-org/dnscache v0.0.0-20130720023526-c70c1f23c5d8/go.mod h1:dniwbG03GafCjFohMDmz6Zc6oCuiqgH6tGNyXTkHzXE= +github.com/vbauerster/mpb/v8 v8.1.6 h1:EswHDkAsy4OQ7QBAmU1MUPz4vHzl6KlINjlh7vJoxvY= +github.com/vbauerster/mpb/v8 v8.1.6/go.mod h1:O9/Wl8X9dUbR63tZ41MLIAxrtNfwlpwUhGkeYugUPW8= github.com/vishvananda/netlink v0.0.0-20181108222139-023a6dafdcdf/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk= github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= github.com/vishvananda/netlink v1.1.1-0.20201029203352-d40f9887b852/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho= github.com/vishvananda/netns v0.0.0-20180720170159-13995c7128cc/go.mod h1:ZjcWmFBXmLKZu9Nxj3WKYEafiSqer2rnvPr0en9UNpI= github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU= github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= -github.com/vmihailenco/msgpack/v4 v4.3.12/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4= github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU= -github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= -github.com/vmihailenco/tagparser v0.1.1 h1:quXMXlA39OCbd2wAdTsGDlK9RkOk6Wuw+x37wVyIuWY= -github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= -github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= -github.com/weppos/publicsuffix-go v0.15.1-0.20210807195340-dc689ff0bb59/go.mod h1:HYux0V0Zi04bHNwOHy4cXJVz/TQjYonnF6aoYhj+3QE= -github.com/weppos/publicsuffix-go v0.15.1-0.20220413065649-906f534b73a4/go.mod h1:HYux0V0Zi04bHNwOHy4cXJVz/TQjYonnF6aoYhj+3QE= github.com/willf/bitset v1.1.11-0.20200630133818-d5bec3311243/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= github.com/willf/bitset v1.1.11/go.mod h1:83CECat5yLh5zVOf4P1ErAgKA5UDvKtgyUABdr3+MjI= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs= +github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g= +github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM= +github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= @@ -1505,46 +898,31 @@ github.com/xeipuuv/gojsonschema v0.0.0-20180618132009-1d523034197f/go.mod h1:5yf github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= -github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= -github.com/yeya24/promlinter v0.1.0/go.mod h1:rs5vtZzeBHqqMwXqFScncpCF6u06lezhZepno9AB1Oc= -github.com/ysmood/goob v0.4.0/go.mod h1:u6yx7ZhS4Exf2MwciFr6nIM8knHQIE22lFpWHnfql18= -github.com/ysmood/got v0.31.3/go.mod h1:pE1l4LOwOBhQg6A/8IAatkGp7uZjnalzrZolnlhhMgY= -github.com/ysmood/gotrace v0.6.0/go.mod h1:TzhIG7nHDry5//eYZDYcTzuJLYQIkykJzCRIo4/dzQM= -github.com/ysmood/gson v0.7.1/go.mod h1:3Kzs5zDl21g5F/BlLTNcuAGAYLKt2lV5G8D1zF3RNmg= -github.com/ysmood/gson v0.7.2/go.mod h1:3Kzs5zDl21g5F/BlLTNcuAGAYLKt2lV5G8D1zF3RNmg= -github.com/ysmood/leakless v0.8.0/go.mod h1:R8iAXPRaG97QJwqxs74RdwzcRHT1SWCGTNqY8q0JvMQ= -github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= -github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= -github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc= -github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= +github.com/ysmood/goob v0.4.0 h1:HsxXhyLBeGzWXnqVKtmT9qM7EuVs/XOgkX7T6r1o1AQ= +github.com/ysmood/gson v0.7.3 h1:QFkWbTH8MxyUTKPkVWAENJhxqdBa4lYTQWqZCiLG6kE= +github.com/ysmood/leakless v0.8.0 h1:BzLrVoiwxikpgEQR0Lk8NyBN5Cit2b1z+u0mgL4ZJak= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43/go.mod h1:aX5oPXxHm3bOH+xeAttToC8pqch2ScQN/JoXYupl6xs= github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50/go.mod h1:NUSPSUX/bi6SeDMUh6brw0nXpxHnc96TguQh0+r/ssA= github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f/go.mod h1:GlGEuHIJweS1mbCqG+7vt2nvWLzLLnRHbXz5JKd/Qbg= -github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= -github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= -github.com/zmap/rc2 v0.0.0-20131011165748-24b9757f5521/go.mod h1:3YZ9o3WnatTIZhuOtot4IcUfzoKVjUHqu6WALIyI0nE= -github.com/zmap/zcertificate v0.0.0-20180516150559-0e3d58b1bac4/go.mod h1:5iU54tB79AMBcySS0R2XIyZBAVmeHranShAFELYx7is= -github.com/zmap/zcrypto v0.0.0-20210811211718-6f9bc4aff20f/go.mod h1:y/9hjFEub4DtQxTHp/pqticBgdYeCwL97vojV3lsvHY= -github.com/zmap/zlint/v3 v3.3.1-0.20211019173530-cb17369b4628/go.mod h1:O+4OXRfNLKqOyDl4eKZ1SBlYudKGUBGRFcv+m1KLr28= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= -go.etcd.io/bbolt v1.3.4/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= -go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU= go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4= -go.etcd.io/etcd v0.0.0-20200513171258-e048e166ab9c/go.mod h1:xCI7ZzBfRuGgBXyXO6yfWfDmlWd35khcWpUa4L0xI/k= +go.etcd.io/bbolt v1.3.7 h1:j+zJOnnEjF/kyHlDDgGnVL/AIqIJPq8UoB2GSNfkUfQ= +go.etcd.io/bbolt v1.3.7/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw= go.etcd.io/etcd v0.5.0-alpha.5.0.20200910180754-dd1b699fc489/go.mod h1:yVHk9ub3CSBatqGNg7GRmsnfLWtoW60w4eDYfh7vHDg= -go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= -go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= -go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= -go.mozilla.org/mozlog v0.0.0-20170222151521-4bb13139d403/go.mod h1:jHoPAGnDrCy6kaI2tAze5Prf0Nr0w/oNkROt2lw3n3o= +go.mongodb.org/mongo-driver v1.7.3/go.mod h1:NqaYOwnXWr5Pm7AOpO5QFxKJ503nbMse/R79oO62zWg= +go.mongodb.org/mongo-driver v1.7.5/go.mod h1:VXEWRZ6URJIkUq2SCAyapmhH0ZLRBP+FT4xhp5Zvxng= +go.mongodb.org/mongo-driver v1.8.3/go.mod h1:0sQWfOeY63QTntERDJJ/0SuKK0T1uVSgKCuAROlKEPY= +go.mongodb.org/mongo-driver v1.10.0/go.mod h1:wsihk0Kdgv8Kqu1Anit4sfK+22vSFbUrAVEYRhCXrA8= +go.mongodb.org/mongo-driver v1.11.1 h1:QP0znIRTuL0jf1oBQoAoM0C6ZJfBK4kx0Uumtv1A7w8= +go.mongodb.org/mongo-driver v1.11.1/go.mod h1:s7p5vEtfbeR1gYi6pnj3c3/urpbLv2T5Sfd6Rp2HBB8= go.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk= go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 h1:CCriYyAfq1Br1aIYettdHZTy8mBTIPo7We18TuO/bak= go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk= @@ -1552,71 +930,35 @@ go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= -go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M= -go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= -go.opentelemetry.io/contrib/propagators v0.19.0/go.mod h1:4QOdZClXISU5S43xZxk5tYaWcpb+lehqfKtE6PK6msE= -go.opentelemetry.io/otel v0.19.0/go.mod h1:j9bF567N9EfomkSidSfmMwIwIBuP37AMAIzVW85OxSg= -go.opentelemetry.io/otel/metric v0.19.0/go.mod h1:8f9fglJPRnXuskQmKpnad31lcLJ2VmNNqIsx/uIwBSc= -go.opentelemetry.io/otel/oteltest v0.19.0/go.mod h1:tI4yxwh8U21v7JD6R3BcA/2+RBoTKFexE/PJ/nSO7IA= -go.opentelemetry.io/otel/trace v0.19.0/go.mod h1:4IXiNextNOpPnRlI4ryK69mn5iC84bjBWZQA5DXz/qg= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= -go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= -go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= -go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= -go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= -go.uber.org/multierr v1.4.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= -go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= -go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= -go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= -go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= -go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= -go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= -goji.io/v3 v3.0.0/go.mod h1:c02FFnNiVNCDo+DpR2IhBQpM9r5G1BG/MkHNTPUJ13U= golang.org/x/crypto v0.0.0-20171113213409-9f005a07e0d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20180501155221-613d6eafa307/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181009213950-7c1a557ab941/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= +golang.org/x/crypto v0.0.0-20190422162423-af44ce270edf/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= +golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191122220453-ac88ee75c92c/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20201124201722-c8d3bf9c5392/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= -golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= -golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= -golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20201216223049-8b5274cf687f/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= -golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 h1:Y/gsMcFOcR+6S6f3YeMKl5g+dZMEWqcz5Czj/GWYbkM= -golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc= +golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -1627,7 +969,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5/go.mod h1:4M0jN8W1tt0AVLNr8HDosyJCDCDuyL9N9+3m7wDWgKw= +golang.org/x/exp v0.0.0-20230206171751-46f607a40771 h1:xP7rWLUr1e1n2xkK5YB4LI0hPEy3LJC6Wk+D4pGlOJg= +golang.org/x/exp v0.0.0-20230206171751-46f607a40771/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -1640,8 +983,6 @@ golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHl golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= @@ -1650,19 +991,15 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/net v0.0.0-20180530234432-1e491301e022/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA= +golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181011144130-49bb7cea24b1/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1679,8 +1016,6 @@ golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191002035440-2ec189313ef0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -1689,70 +1024,32 @@ golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200421231249-e086a090c8fd/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= -golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.0.0-20220617184016-355a448f1bc9/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.0.0-20220708220712-1185a9018129/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b h1:ZmngSVLe/wycRns9MKikG9OWIEjGcGAkacif7oYQaUY= -golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= +golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= +golang.org/x/net v0.6.0 h1:L4ZwwTvKW9gr0ZMS1yrHD9GZhIuVjOBBnaKH+SPQK0Q= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= -golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= -golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= -golang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= -golang.org/x/oauth2 v0.0.0-20220622183110-fd043fe589d2/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= -golang.org/x/oauth2 v0.0.0-20220718184931-c8730f7fcb92/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= -golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= +golang.org/x/oauth2 v0.3.0/go.mod h1:rQrIauxkUhJ6CuwEXwymO2/eh4xz2ZWF1nBkcxS+tGk= +golang.org/x/oauth2 v0.5.0 h1:HuArIo48skDwlrvM3sEdHXElYslAMsf3KwRkkW4MC4s= +golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1765,28 +1062,26 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190514135907-3a4b5fb9f71f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190522044717-8097e1b27ff5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190602015325-4c4f7f33c9ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1796,15 +1091,11 @@ golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190812073006-9eafafc0a87e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191115151921-52ab43148777/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1814,7 +1105,6 @@ golang.org/x/sys v0.0.0-20191210023423-ac6580df4449/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200120151820-655fe14d7479/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1824,20 +1114,11 @@ golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200817155316-9781c653f443/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200916030750-2334cc1a136f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200922070232-aee5d888a860/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1848,70 +1129,33 @@ golang.org/x/sys v0.0.0-20201117170446-d9b008d0a637/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201202213521-69691e467435/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210228012217-479acdf4ea46/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210816074244-15123e1e1f71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210915083310-ed5796bab164/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210917161153-d61c044b1678/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220624220833-87e55d714810/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220907062415-87db552b00fd/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220909162455-aba9fc2a8ff2 h1:wM1k/lXfpc5HdkJJyW9GELpd8ERGdnh8sMGL6Gzq3Ho= -golang.org/x/sys v0.0.0-20220909162455-aba9fc2a8ff2/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.0.0-20220526004731-065cf7ba2467 h1:CBpWXWQpIRjzmkkA+M7q9Fqnwd2mZr3AFqexg8YTfoM= -golang.org/x/term v0.0.0-20220526004731-065cf7ba2467/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= +golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1920,38 +1164,34 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9 h1:ftMN5LMiBFjbzleLqtoBZk7KdJwhuybIU+FckUHgoyQ= -golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.2.0 h1:52I/1L54xyEQAYdtcSuxtiT84KGYTBGXwayxmIpNJhE= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190110163146-51295c7ec13a/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190307163923-6a08e3108db3/go.mod h1:25r3+/G6/xytQM8iWZKq3Hn0kr0rgFKPUNVEL/dr3z4= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190311215038-5c2858a9cfe5/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190321232350-e250d351ecad/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190322203728-c1a832b0ad89/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190329151228-23e29df326fe/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190416151739-9c9e1878f421/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190420181800-aa740d480789/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190531172133-b3315ee88b7d/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= @@ -1959,160 +1199,60 @@ golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgw golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190706070813-72ffa07ba3db/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI= golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190910044552-dd2b5c81c578/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190916130336-e45ffcd953cc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191010075000-0337d82405ff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200117220505-0cba7a3a9ee9/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200308013534-11ec41452d41/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200324003944-a576cf524670/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= -golang.org/x/tools v0.0.0-20200329025819-fd4102a86c65/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= -golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= -golang.org/x/tools v0.0.0-20200414032229-332987a829c3/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200422022333-3d57cf2e726e/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200426102838-f3a5411a4c3b/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200616133436-c1934b75d054/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200622203043-20e05c1c8ffa/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200624225443-88f3c62a19ff/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200625211823-6506e20df31f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200626171337-aa94e735be7f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200630154851-b2d8b0336632/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200706234117-b22de6825cf7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200724022722-7017fd6b1305/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200812195022-5ae4c3c160a0/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200820010801-b793a1359eac/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200831203904-5a2aa26beb65/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= -golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= golang.org/x/tools v0.0.0-20200916195026-c9a70fc28ce3/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= -golang.org/x/tools v0.0.0-20201001104356-43ebab892c4c/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= -golang.org/x/tools v0.0.0-20201002184944-ecd9fd270d5d/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= -golang.org/x/tools v0.0.0-20201023174141-c8cfbd0f21e6/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201028025901-8cd080b735b3/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201114224030-61ea331ec02b/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201118003311-bd56c0adb394/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201230224404-63754364767c/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210101214203-2dba1e4ea05c/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210104081019-d8d6ddbec6ee/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210112230658-8b4aab62c064/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= -golang.org/x/tools v0.1.1-0.20210205202024-ef80cdb6ec6d/go.mod h1:9bzcO0MWcOuT0tm1iBGzDVPshzfwoVvREIui8C+MHqU= -golang.org/x/tools v0.1.1-0.20210302220138-2ac05c832e1a/go.mod h1:9bzcO0MWcOuT0tm1iBGzDVPshzfwoVvREIui8C+MHqU= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.6/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= -golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= -golang.org/x/tools v0.1.11/go.mod h1:SgwaegtQh8clINPpECJMqnxLv9I09HLqnW3RMqW0CA4= -golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.4.0 h1:7mTAgkunk3fr4GAloyyCasadO6h9zSsQZbwvcaIciV4= +golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= -golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= google.golang.org/api v0.0.0-20160322025152-9bf6e6e569ff/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.10.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= -google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= -google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= -google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= -google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= -google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= -google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= -google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8= -google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo= -google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4= -google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw= -google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU= -google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k= -google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= -google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= -google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI= -google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I= -google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo= -google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g= -google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA= -google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8= -google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs= -google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= -google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw= -google.golang.org/api v0.80.0/go.mod h1:xY3nI94gbvBrE0J6NHXhxOmW97HG7Khjkku6AFB3Hyg= -google.golang.org/api v0.84.0/go.mod h1:NTsGnUFJMYROtiquksZHBWtHfeMC7iYthki7Eq3pa8o= -google.golang.org/api v0.85.0/go.mod h1:AqZf8Ep9uZ2pyTvgL+x0D3Zt0eoT9b5E8fmzfu6FO2g= -google.golang.org/api v0.86.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw= -google.golang.org/api v0.95.0/go.mod h1:eADj+UBuxkh5zlrSntJghuNeg8HwQ1w5lTKkuqaETEI= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.2/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/cloud v0.0.0-20151119220103-975617b05ea8/go.mod h1:0H1ncTHf11KCFhTc/+EFRbzSCOZx+VUbRMk55Yv5MYk= -google.golang.org/genproto v0.0.0-20170818010345-ee236bd376b0/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20181107211654-5fc9ac540362/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= @@ -2121,7 +1261,6 @@ google.golang.org/genproto v0.0.0-20190522204451-c2c4e71fbf69/go.mod h1:z3L6/3dT google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= -google.golang.org/genproto v0.0.0-20190927181202-20e1ac93f88c/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= @@ -2132,79 +1271,15 @@ google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvx google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20200527145253-8367513e4ece/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= -google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= -google.golang.org/genproto v0.0.0-20200626011028-ee7919e894b5/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200707001353-8e8330bf89df/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210329143202-679c6ae281ee/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= -google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= -google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= -google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= -google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= -google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= -google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= -google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= -google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w= -google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= -google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= -google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= -google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= -google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E= -google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= -google.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= -google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= -google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= -google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= -google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= -google.golang.org/genproto v0.0.0-20220518221133-4f43b3371335/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= -google.golang.org/genproto v0.0.0-20220523171625-347a074981d8/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= -google.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= -google.golang.org/genproto v0.0.0-20220616135557-88e70c0c3a90/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= -google.golang.org/genproto v0.0.0-20220617124728-180714bec0ad/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= -google.golang.org/genproto v0.0.0-20220624142145-8cd45d7dbd1f/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= -google.golang.org/genproto v0.0.0-20220628213854-d9e0b6570c03/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= -google.golang.org/genproto v0.0.0-20220720214146-176da50484ac h1:EOa+Yrhx1C0O+4pHeXeWrCwdI0tWI6IfUU56Vebs9wQ= -google.golang.org/genproto v0.0.0-20220720214146-176da50484ac/go.mod h1:GkXuJDJ6aQ7lnJcRF+SJVgFdQhypqgl3LB1C9vabdRE= +google.golang.org/genproto v0.0.0-20230209215440-0dfe4f8abfcc h1:ijGwO+0vL2hJt5gaygqP2j6PfflOBrRot0IczKbmtio= +google.golang.org/genproto v0.0.0-20230209215440-0dfe4f8abfcc/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= -google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -2216,34 +1291,14 @@ google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQ google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= -google.golang.org/grpc v1.29.0/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= -google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= -google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= -google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= -google.golang.org/grpc v1.41.0/go.mod h1:U3l9uK9J0sini8mHphKoXyaqDA/8VyGnDee1zzIUK6k= -google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= -google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= -google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/grpc v1.48.0 h1:rQOsyJ/8+ufEDJd/Gdsz7HG220Mh9HAhFHRGnIjda0w= -google.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= +google.golang.org/grpc v1.53.0 h1:LAv2ds7cmFV/XTS3XG1NneeENYrXGmorPxsBbptIjNc= +google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -2263,7 +1318,6 @@ google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqw gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/alexcesaro/statsd.v2 v2.0.0 h1:FXkZSCZIH17vLCO5sO2UucTHsH9pc+17F6pl3JVCwMc= -gopkg.in/alexcesaro/statsd.v2 v2.0.0/go.mod h1:i0ubccKGzBVNBpdGV5MocxyA/XlLUJzA7SLonnE4drU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20141024133853-64131543e789/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -2272,15 +1326,12 @@ gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= -gopkg.in/cheggaaa/pb.v1 v1.0.28/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= -gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= +gopkg.in/go-jose/go-jose.v2 v2.6.1 h1:qEzJlIDmG9q5VO0M/o8tGS65QMHMS1w01TQJB1VPJ4U= +gopkg.in/go-jose/go-jose.v2 v2.6.1/go.mod h1:zzZDPkNNw/c9IE7Z9jr11mBZQhKQTMzoEEIoEdZlFBI= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= -gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/ini.v1 v1.63.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= @@ -2289,19 +1340,19 @@ gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76 gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI= gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.6/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= @@ -2309,16 +1360,15 @@ gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= -gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= +gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o= +gotest.tools/v3 v3.4.0/go.mod h1:CtbdzLSsqVhDgMtKsx03ird5YTGB3ar27v0u/yKBW5g= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.2.1/go.mod h1:lPVVZ2BS5TfnjLyizF7o7hv7j9/L+8cZY2hLyjP9cGY= k8s.io/api v0.20.1/go.mod h1:KqwcCVogGxQY3nBlRpwt+wpAMF/KjaCc7RpywacvqUo= k8s.io/api v0.20.4/go.mod h1:++lNL1AJMkDymriNniQsWRkMDzRaX2Y/POTUi8yvqYQ= k8s.io/api v0.20.6/go.mod h1:X9e8Qag6JV/bL5G6bU8sdVRltWKmdHsFUGS3eVndqE8= @@ -2345,14 +1395,11 @@ k8s.io/gengo v0.0.0-20201113003025-83324d819ded/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAE k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= k8s.io/klog/v2 v2.4.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= +k8s.io/klog/v2 v2.90.0 h1:VkTxIV/FjRXn1fgNNcKGM8cfmL1Z33ZjXRTVxKCoF5M= k8s.io/kube-openapi v0.0.0-20200805222855-6aeccd4b50c6/go.mod h1:UuqjUnNftUyPE5H64/qeyjQoUZhGpeFDVdxjTeEVN2o= k8s.io/kube-openapi v0.0.0-20201113171705-d219536bb9fd/go.mod h1:WOJ3KddDSol4tAGcJo0Tvi+dK12EcqSLqcWsryKMpfM= k8s.io/kubernetes v1.13.0/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk= k8s.io/utils v0.0.0-20201110183641-67b214c5f920/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= -mvdan.cc/gofumpt v0.1.1/go.mod h1:yXG1r1WqZVKWbVRtBWKWX9+CxGYfA51nSomhM0woR48= -mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed/go.mod h1:Xkxe497xwlCKkIaQYRfC7CSLworTXY9RMqwhhCm+8Nc= -mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b/go.mod h1:2odslEg/xrtNQqCYg2/jCoyKnw3vv5biOc3JnIcYfL4= -mvdan.cc/unparam v0.0.0-20210104141923-aac4ce9116a7/go.mod h1:hBpJkZE8H/sb+VRFvw2+rBpHNsTBcvSpk61hr8mzXZE= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/internal/image/docker_list.go b/internal/image/docker_list.go index 8afc406282..617a451aa9 100644 --- a/internal/image/docker_list.go +++ b/internal/image/docker_list.go @@ -4,7 +4,7 @@ import ( "context" "fmt" - "github.com/containers/image/v5/manifest" + "github.com/containers/image/v5/internal/manifest" "github.com/containers/image/v5/types" ) diff --git a/internal/image/docker_schema1_test.go b/internal/image/docker_schema1_test.go index 396b51ed9a..553db34c71 100644 --- a/internal/image/docker_schema1_test.go +++ b/internal/image/docker_schema1_test.go @@ -153,13 +153,13 @@ func TestManifestSchema1Serialize(t *testing.T) { } { serialized, err := m.serialize() require.NoError(t, err) - var contents map[string]interface{} + var contents map[string]any err = json.Unmarshal(serialized, &contents) require.NoError(t, err) original, err := os.ReadFile("fixtures/schema1.json") require.NoError(t, err) - var originalContents map[string]interface{} + var originalContents map[string]any err = json.Unmarshal(original, &originalContents) require.NoError(t, err) @@ -465,7 +465,7 @@ func TestManifestSchema1ConvertToSchema2(t *testing.T) { byHandJSON, err := os.ReadFile("fixtures/schema1-to-schema2.json") require.NoError(t, err) - var converted, byHand map[string]interface{} + var converted, byHand map[string]any err = json.Unmarshal(byHandJSON, &byHand) require.NoError(t, err) err = json.Unmarshal(convertedJSON, &converted) @@ -479,8 +479,8 @@ func TestManifestSchema1ConvertToSchema2(t *testing.T) { byHandConfig, err := os.ReadFile("fixtures/schema1-to-schema2-config.json") require.NoError(t, err) - converted = map[string]interface{}{} - byHand = map[string]interface{}{} + converted = map[string]any{} + byHand = map[string]any{} err = json.Unmarshal(byHandConfig, &byHand) require.NoError(t, err) err = json.Unmarshal(convertedConfig, &converted) @@ -558,7 +558,7 @@ func TestManifestSchema1ConvertToManifestOCI1(t *testing.T) { byHandJSON, err := os.ReadFile("fixtures/schema1-to-oci1.json") require.NoError(t, err) - var converted, byHand map[string]interface{} + var converted, byHand map[string]any err = json.Unmarshal(byHandJSON, &byHand) require.NoError(t, err) err = json.Unmarshal(convertedJSON, &converted) @@ -572,8 +572,8 @@ func TestManifestSchema1ConvertToManifestOCI1(t *testing.T) { byHandConfig, err := os.ReadFile("fixtures/schema1-to-oci1-config.json") require.NoError(t, err) - converted = map[string]interface{}{} - byHand = map[string]interface{}{} + converted = map[string]any{} + byHand = map[string]any{} err = json.Unmarshal(byHandConfig, &byHand) require.NoError(t, err) err = json.Unmarshal(convertedConfig, &converted) diff --git a/internal/image/docker_schema2.go b/internal/image/docker_schema2.go index 23a21999aa..15c9c22793 100644 --- a/internal/image/docker_schema2.go +++ b/internal/image/docker_schema2.go @@ -381,7 +381,7 @@ func v1ConfigFromConfigJSON(configJSON []byte, v1ID, parentV1ID string, throwawa delete(rawContents, "rootfs") delete(rawContents, "history") - updates := map[string]interface{}{"id": v1ID} + updates := map[string]any{"id": v1ID} if parentV1ID != "" { updates["parent"] = parentV1ID } diff --git a/internal/image/docker_schema2_test.go b/internal/image/docker_schema2_test.go index d810a07cde..a226060247 100644 --- a/internal/image/docker_schema2_test.go +++ b/internal/image/docker_schema2_test.go @@ -20,6 +20,7 @@ import ( imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "golang.org/x/exp/slices" ) func manifestSchema2FromFixture(t *testing.T, src types.ImageSource, fixture string, mustFail bool) genericManifest { @@ -91,13 +92,13 @@ func TestManifestSchema2Serialize(t *testing.T) { } { serialized, err := m.serialize() require.NoError(t, err) - var contents map[string]interface{} + var contents map[string]any err = json.Unmarshal(serialized, &contents) require.NoError(t, err) original, err := os.ReadFile("fixtures/schema2.json") require.NoError(t, err) - var originalContents map[string]interface{} + var originalContents map[string]any err = json.Unmarshal(original, &originalContents) require.NoError(t, err) @@ -438,11 +439,11 @@ func modifiedLayerInfos(t *testing.T, input []types.BlobInfo) ([]types.BlobInfo, require.NoError(t, err) oldDigest[len(oldDigest)-1] ^= 1 b2.Digest = digest.NewDigestFromEncoded(b2.Digest.Algorithm(), hex.EncodeToString(oldDigest)) - b2.Size = b2.Size ^ 1 + b2.Size ^= 1 modified = append(modified, b2) } - copy := append([]types.BlobInfo{}, modified...) + copy := slices.Clone(modified) return modified, copy } @@ -522,7 +523,7 @@ func TestConvertToManifestOCI(t *testing.T) { byHandJSON, err := os.ReadFile("fixtures/schema2-to-oci1.json") require.NoError(t, err) - var converted, byHand map[string]interface{} + var converted, byHand map[string]any err = json.Unmarshal(byHandJSON, &byHand) require.NoError(t, err) err = json.Unmarshal(convertedJSON, &converted) @@ -543,7 +544,7 @@ func TestConvertToManifestOCIAllMediaTypes(t *testing.T) { byHandJSON, err := os.ReadFile("fixtures/schema2-all-media-types-to-oci1.json") require.NoError(t, err) - var converted, byHand map[string]interface{} + var converted, byHand map[string]any err = json.Unmarshal(byHandJSON, &byHand) require.NoError(t, err) err = json.Unmarshal(convertedJSON, &converted) @@ -577,7 +578,7 @@ func TestConvertToManifestSchema1(t *testing.T) { // memoryDest, not from originalSrc, is used. byDockerJSON, err := os.ReadFile("fixtures/schema2-to-schema1-by-docker.json") require.NoError(t, err) - var converted, byDocker map[string]interface{} + var converted, byDocker map[string]any err = json.Unmarshal(byDockerJSON, &byDocker) require.NoError(t, err) err = json.Unmarshal(convertedJSON, &converted) diff --git a/internal/image/oci_index.go b/internal/image/oci_index.go index f4f76622c5..0e945c8519 100644 --- a/internal/image/oci_index.go +++ b/internal/image/oci_index.go @@ -4,7 +4,7 @@ import ( "context" "fmt" - "github.com/containers/image/v5/manifest" + "github.com/containers/image/v5/internal/manifest" "github.com/containers/image/v5/types" ) diff --git a/internal/image/oci_test.go b/internal/image/oci_test.go index 53e8e738f1..65df8a671e 100644 --- a/internal/image/oci_test.go +++ b/internal/image/oci_test.go @@ -94,13 +94,13 @@ func TestManifestOCI1Serialize(t *testing.T) { } { serialized, err := m.serialize() require.NoError(t, err) - var contents map[string]interface{} + var contents map[string]any err = json.Unmarshal(serialized, &contents) require.NoError(t, err) original, err := os.ReadFile("fixtures/oci1.json") require.NoError(t, err) - var originalContents map[string]interface{} + var originalContents map[string]any err = json.Unmarshal(original, &originalContents) require.NoError(t, err) @@ -459,7 +459,7 @@ func TestManifestOCI1ConvertToManifestSchema1(t *testing.T) { byHandJSON, err := os.ReadFile("fixtures/oci1-to-schema1.json") require.NoError(t, err) - var converted, byHand map[string]interface{} + var converted, byHand map[string]any err = json.Unmarshal(byHandJSON, &byHand) require.NoError(t, err) err = json.Unmarshal(convertedJSON, &converted) @@ -534,7 +534,7 @@ func TestConvertToManifestSchema2(t *testing.T) { byHandJSON, err := os.ReadFile("fixtures/oci1-to-schema2.json") require.NoError(t, err) - var converted, byHand map[string]interface{} + var converted, byHand map[string]any err = json.Unmarshal(byHandJSON, &byHand) require.NoError(t, err) err = json.Unmarshal(convertedJSON, &converted) diff --git a/internal/image/sourced.go b/internal/image/sourced.go index dc09a9e04b..661891aa55 100644 --- a/internal/image/sourced.go +++ b/internal/image/sourced.go @@ -10,7 +10,7 @@ import ( ) // FromReference returns a types.ImageCloser implementation for the default instance reading from reference. -// If reference poitns to a manifest list, .Manifest() still returns the manifest list, +// If reference points to a manifest list, .Manifest() still returns the manifest list, // but other methods transparently return data from an appropriate image instance. // // The caller must call .Close() on the returned ImageCloser. diff --git a/internal/imagesource/stubs/stubs.go b/internal/imagesource/stubs/stubs.go index 0ce6fd51f7..cb345395e2 100644 --- a/internal/imagesource/stubs/stubs.go +++ b/internal/imagesource/stubs/stubs.go @@ -15,7 +15,7 @@ // // Second, there are stubs with a constructor, like NoGetBlobAtInitialize. The Initialize marker // means that a constructor must be called: - +// // type yourSource struct { // stubs.NoGetBlobAtInitialize // … diff --git a/internal/iolimits/iolimits_test.go b/internal/iolimits/iolimits_test.go index d2fc65181c..630b38ca55 100644 --- a/internal/iolimits/iolimits_test.go +++ b/internal/iolimits/iolimits_test.go @@ -10,6 +10,7 @@ import ( ) func TestReadAtMost(t *testing.T) { + rng := rand.New(rand.NewSource(0)) for _, c := range []struct { input, limit int shouldSucceed bool @@ -23,7 +24,7 @@ func TestReadAtMost(t *testing.T) { {bytes.MinRead*5 + 1, bytes.MinRead * 5, false}, } { input := make([]byte, c.input) - _, err := rand.Read(input) + _, err := rng.Read(input) require.NoError(t, err) result, err := ReadAtMost(bytes.NewReader(input), c.limit) if c.shouldSucceed { diff --git a/internal/manifest/common.go b/internal/manifest/common.go new file mode 100644 index 0000000000..1f2ccb5286 --- /dev/null +++ b/internal/manifest/common.go @@ -0,0 +1,72 @@ +package manifest + +import ( + "encoding/json" + "fmt" +) + +// AllowedManifestFields is a bit mask of “essential” manifest fields that ValidateUnambiguousManifestFormat +// can expect to be present. +type AllowedManifestFields int + +const ( + AllowedFieldConfig AllowedManifestFields = 1 << iota + AllowedFieldFSLayers + AllowedFieldHistory + AllowedFieldLayers + AllowedFieldManifests + AllowedFieldFirstUnusedBit // Keep this at the end! +) + +// ValidateUnambiguousManifestFormat rejects manifests (incl. multi-arch) that look like more than +// one kind we currently recognize, i.e. if they contain any of the known “essential” format fields +// other than the ones the caller specifically allows. +// expectedMIMEType is used only for diagnostics. +// NOTE: The caller should do the non-heuristic validations (e.g. check for any specified format +// identification/version, or other “magic numbers”) before calling this, to cleanly reject unambiguous +// data that just isn’t what was expected, as opposed to actually ambiguous data. +func ValidateUnambiguousManifestFormat(manifest []byte, expectedMIMEType string, + allowed AllowedManifestFields) error { + if allowed >= AllowedFieldFirstUnusedBit { + return fmt.Errorf("internal error: invalid allowedManifestFields value %#v", allowed) + } + // Use a private type to decode, not just a map[string]any, because we want + // to also reject case-insensitive matches (which would be used by Go when really decoding + // the manifest). + // (It is expected that as manifest formats are added or extended over time, more fields will be added + // here.) + detectedFields := struct { + Config any `json:"config"` + FSLayers any `json:"fsLayers"` + History any `json:"history"` + Layers any `json:"layers"` + Manifests any `json:"manifests"` + }{} + if err := json.Unmarshal(manifest, &detectedFields); err != nil { + // The caller was supposed to already validate version numbers, so this should not happen; + // let’s not bother with making this error “nice”. + return err + } + unexpected := []string{} + // Sadly this isn’t easy to automate in Go, without reflection. So, copy&paste. + if detectedFields.Config != nil && (allowed&AllowedFieldConfig) == 0 { + unexpected = append(unexpected, "config") + } + if detectedFields.FSLayers != nil && (allowed&AllowedFieldFSLayers) == 0 { + unexpected = append(unexpected, "fsLayers") + } + if detectedFields.History != nil && (allowed&AllowedFieldHistory) == 0 { + unexpected = append(unexpected, "history") + } + if detectedFields.Layers != nil && (allowed&AllowedFieldLayers) == 0 { + unexpected = append(unexpected, "layers") + } + if detectedFields.Manifests != nil && (allowed&AllowedFieldManifests) == 0 { + unexpected = append(unexpected, "manifests") + } + if len(unexpected) != 0 { + return fmt.Errorf(`rejecting ambiguous manifest, unexpected fields %#v in supposedly %s`, + unexpected, expectedMIMEType) + } + return nil +} diff --git a/internal/manifest/common_test.go b/internal/manifest/common_test.go new file mode 100644 index 0000000000..553cc36295 --- /dev/null +++ b/internal/manifest/common_test.go @@ -0,0 +1,91 @@ +package manifest + +import ( + "bytes" + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestValidateUnambiguousManifestFormat(t *testing.T) { + const allAllowedFields = AllowedFieldFirstUnusedBit - 1 + const mt = "text/plain" // Just some MIME type that shows up in error messages + + type test struct { + manifest string + allowed AllowedManifestFields + } + + // Smoke tests: Success + for _, c := range []test{ + {"{}", allAllowedFields}, + {"{}", 0}, + } { + err := ValidateUnambiguousManifestFormat([]byte(c.manifest), mt, c.allowed) + assert.NoError(t, err, c) + } + // Smoke tests: Failure + for _, c := range []test{ + {"{}", AllowedFieldFirstUnusedBit}, // Invalid "allowed" + {"@", allAllowedFields}, // Invalid JSON + } { + err := ValidateUnambiguousManifestFormat([]byte(c.manifest), mt, c.allowed) + assert.Error(t, err, c) + } + + fields := map[AllowedManifestFields]string{ + AllowedFieldConfig: "config", + AllowedFieldFSLayers: "fsLayers", + AllowedFieldHistory: "history", + AllowedFieldLayers: "layers", + AllowedFieldManifests: "manifests", + } + // Ensure this test covers all defined AllowedManifestFields values + allFields := AllowedManifestFields(0) + for k := range fields { + allFields |= k + } + assert.Equal(t, allAllowedFields, allFields) + + // Every single field is allowed by its bit, and rejected by any other bit + for bit, fieldName := range fields { + json := []byte(fmt.Sprintf(`{"%s":[]}`, fieldName)) + err := ValidateUnambiguousManifestFormat(json, mt, bit) + assert.NoError(t, err, fieldName) + err = ValidateUnambiguousManifestFormat(json, mt, allAllowedFields^bit) + assert.Error(t, err, fieldName) + } +} + +// Test that parser() rejects all of the provided manifest fixtures. +// Intended to help test manifest parsers' detection of schema mismatches. +func testManifestFixturesAreRejected(t *testing.T, parser func([]byte) error, fixtures []string) { + for _, fixture := range fixtures { + manifest, err := os.ReadFile(filepath.Join("testdata", fixture)) + require.NoError(t, err, fixture) + err = parser(manifest) + assert.Error(t, err, fixture) + } +} + +// Test that parser() rejects validManifest with an added top-level field with any of the provided field names. +// Intended to help test callers of validateUnambiguousManifestFormat. +func testValidManifestWithExtraFieldsIsRejected(t *testing.T, parser func([]byte) error, + validManifest []byte, fields []string) { + for _, field := range fields { + // end (the final '}') is not always at len(validManifest)-1 because the manifest can end with + // white space. + end := bytes.LastIndexByte(validManifest, '}') + require.NotEqual(t, end, -1) + updatedManifest := []byte(string(validManifest[:end]) + + fmt.Sprintf(`,"%s":[]}`, field)) + err := parser(updatedManifest) + // Make sure it is the error from validateUnambiguousManifestFormat, not something that + // went wrong with creating updatedManifest. + assert.ErrorContains(t, err, "rejecting ambiguous manifest", field) + } +} diff --git a/internal/manifest/docker_schema2.go b/internal/manifest/docker_schema2.go new file mode 100644 index 0000000000..68d0796978 --- /dev/null +++ b/internal/manifest/docker_schema2.go @@ -0,0 +1,15 @@ +package manifest + +import ( + "github.com/opencontainers/go-digest" +) + +// Schema2Descriptor is a “descriptor” in docker/distribution schema 2. +// +// This is publicly visible as c/image/manifest.Schema2Descriptor. +type Schema2Descriptor struct { + MediaType string `json:"mediaType"` + Size int64 `json:"size"` + Digest digest.Digest `json:"digest"` + URLs []string `json:"urls,omitempty"` +} diff --git a/internal/manifest/docker_schema2_list.go b/internal/manifest/docker_schema2_list.go new file mode 100644 index 0000000000..76261e436a --- /dev/null +++ b/internal/manifest/docker_schema2_list.go @@ -0,0 +1,255 @@ +package manifest + +import ( + "encoding/json" + "fmt" + + platform "github.com/containers/image/v5/internal/pkg/platform" + "github.com/containers/image/v5/types" + "github.com/opencontainers/go-digest" + imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" + "golang.org/x/exp/slices" +) + +// Schema2PlatformSpec describes the platform which a particular manifest is +// specialized for. +// This is publicly visible as c/image/manifest.Schema2PlatformSpec. +type Schema2PlatformSpec struct { + Architecture string `json:"architecture"` + OS string `json:"os"` + OSVersion string `json:"os.version,omitempty"` + OSFeatures []string `json:"os.features,omitempty"` + Variant string `json:"variant,omitempty"` + Features []string `json:"features,omitempty"` // removed in OCI +} + +// Schema2ManifestDescriptor references a platform-specific manifest. +// This is publicly visible as c/image/manifest.Schema2ManifestDescriptor. +type Schema2ManifestDescriptor struct { + Schema2Descriptor + Platform Schema2PlatformSpec `json:"platform"` +} + +// Schema2ListPublic is a list of platform-specific manifests. +// This is publicly visible as c/image/manifest.Schema2List. +// Internal users should usually use Schema2List instead. +type Schema2ListPublic struct { + SchemaVersion int `json:"schemaVersion"` + MediaType string `json:"mediaType"` + Manifests []Schema2ManifestDescriptor `json:"manifests"` +} + +// MIMEType returns the MIME type of this particular manifest list. +func (list *Schema2ListPublic) MIMEType() string { + return list.MediaType +} + +// Instances returns a slice of digests of the manifests that this list knows of. +func (list *Schema2ListPublic) Instances() []digest.Digest { + results := make([]digest.Digest, len(list.Manifests)) + for i, m := range list.Manifests { + results[i] = m.Digest + } + return results +} + +// Instance returns the ListUpdate of a particular instance in the list. +func (list *Schema2ListPublic) Instance(instanceDigest digest.Digest) (ListUpdate, error) { + for _, manifest := range list.Manifests { + if manifest.Digest == instanceDigest { + return ListUpdate{ + Digest: manifest.Digest, + Size: manifest.Size, + MediaType: manifest.MediaType, + }, nil + } + } + return ListUpdate{}, fmt.Errorf("unable to find instance %s passed to Schema2List.Instances", instanceDigest) +} + +// UpdateInstances updates the sizes, digests, and media types of the manifests +// which the list catalogs. +func (list *Schema2ListPublic) UpdateInstances(updates []ListUpdate) error { + if len(updates) != len(list.Manifests) { + return fmt.Errorf("incorrect number of update entries passed to Schema2List.UpdateInstances: expected %d, got %d", len(list.Manifests), len(updates)) + } + for i := range updates { + if err := updates[i].Digest.Validate(); err != nil { + return fmt.Errorf("update %d of %d passed to Schema2List.UpdateInstances contained an invalid digest: %w", i+1, len(updates), err) + } + list.Manifests[i].Digest = updates[i].Digest + if updates[i].Size < 0 { + return fmt.Errorf("update %d of %d passed to Schema2List.UpdateInstances had an invalid size (%d)", i+1, len(updates), updates[i].Size) + } + list.Manifests[i].Size = updates[i].Size + if updates[i].MediaType == "" { + return fmt.Errorf("update %d of %d passed to Schema2List.UpdateInstances had no media type (was %q)", i+1, len(updates), list.Manifests[i].MediaType) + } + list.Manifests[i].MediaType = updates[i].MediaType + } + return nil +} + +// ChooseInstance parses blob as a schema2 manifest list, and returns the digest +// of the image which is appropriate for the current environment. +func (list *Schema2ListPublic) ChooseInstance(ctx *types.SystemContext) (digest.Digest, error) { + wantedPlatforms, err := platform.WantedPlatforms(ctx) + if err != nil { + return "", fmt.Errorf("getting platform information %#v: %w", ctx, err) + } + for _, wantedPlatform := range wantedPlatforms { + for _, d := range list.Manifests { + imagePlatform := imgspecv1.Platform{ + Architecture: d.Platform.Architecture, + OS: d.Platform.OS, + OSVersion: d.Platform.OSVersion, + OSFeatures: slices.Clone(d.Platform.OSFeatures), + Variant: d.Platform.Variant, + } + if platform.MatchesPlatform(imagePlatform, wantedPlatform) { + return d.Digest, nil + } + } + } + return "", fmt.Errorf("no image found in manifest list for architecture %s, variant %q, OS %s", wantedPlatforms[0].Architecture, wantedPlatforms[0].Variant, wantedPlatforms[0].OS) +} + +// Serialize returns the list in a blob format. +// NOTE: Serialize() does not in general reproduce the original blob if this object was loaded from one, even if no modifications were made! +func (list *Schema2ListPublic) Serialize() ([]byte, error) { + buf, err := json.Marshal(list) + if err != nil { + return nil, fmt.Errorf("marshaling Schema2List %#v: %w", list, err) + } + return buf, nil +} + +// Schema2ListPublicFromComponents creates a Schema2 manifest list instance from the +// supplied data. +// This is publicly visible as c/image/manifest.Schema2ListFromComponents. +func Schema2ListPublicFromComponents(components []Schema2ManifestDescriptor) *Schema2ListPublic { + list := Schema2ListPublic{ + SchemaVersion: 2, + MediaType: DockerV2ListMediaType, + Manifests: make([]Schema2ManifestDescriptor, len(components)), + } + for i, component := range components { + m := Schema2ManifestDescriptor{ + Schema2Descriptor{ + MediaType: component.MediaType, + Size: component.Size, + Digest: component.Digest, + URLs: slices.Clone(component.URLs), + }, + Schema2PlatformSpec{ + Architecture: component.Platform.Architecture, + OS: component.Platform.OS, + OSVersion: component.Platform.OSVersion, + OSFeatures: slices.Clone(component.Platform.OSFeatures), + Variant: component.Platform.Variant, + Features: slices.Clone(component.Platform.Features), + }, + } + list.Manifests[i] = m + } + return &list +} + +// Schema2ListPublicClone creates a deep copy of the passed-in list. +// This is publicly visible as c/image/manifest.Schema2ListClone. +func Schema2ListPublicClone(list *Schema2ListPublic) *Schema2ListPublic { + return Schema2ListPublicFromComponents(list.Manifests) +} + +// ToOCI1Index returns the list encoded as an OCI1 index. +func (list *Schema2ListPublic) ToOCI1Index() (*OCI1IndexPublic, error) { + components := make([]imgspecv1.Descriptor, 0, len(list.Manifests)) + for _, manifest := range list.Manifests { + converted := imgspecv1.Descriptor{ + MediaType: manifest.MediaType, + Size: manifest.Size, + Digest: manifest.Digest, + URLs: slices.Clone(manifest.URLs), + Platform: &imgspecv1.Platform{ + OS: manifest.Platform.OS, + Architecture: manifest.Platform.Architecture, + OSFeatures: slices.Clone(manifest.Platform.OSFeatures), + OSVersion: manifest.Platform.OSVersion, + Variant: manifest.Platform.Variant, + }, + } + components = append(components, converted) + } + oci := OCI1IndexPublicFromComponents(components, nil) + return oci, nil +} + +// ToSchema2List returns the list encoded as a Schema2 list. +func (list *Schema2ListPublic) ToSchema2List() (*Schema2ListPublic, error) { + return Schema2ListPublicClone(list), nil +} + +// Schema2ListPublicFromManifest creates a Schema2 manifest list instance from marshalled +// JSON, presumably generated by encoding a Schema2 manifest list. +// This is publicly visible as c/image/manifest.Schema2ListFromManifest. +func Schema2ListPublicFromManifest(manifest []byte) (*Schema2ListPublic, error) { + list := Schema2ListPublic{ + Manifests: []Schema2ManifestDescriptor{}, + } + if err := json.Unmarshal(manifest, &list); err != nil { + return nil, fmt.Errorf("unmarshaling Schema2List %q: %w", string(manifest), err) + } + if err := ValidateUnambiguousManifestFormat(manifest, DockerV2ListMediaType, + AllowedFieldManifests); err != nil { + return nil, err + } + return &list, nil +} + +// Clone returns a deep copy of this list and its contents. +func (list *Schema2ListPublic) Clone() ListPublic { + return Schema2ListPublicClone(list) +} + +// ConvertToMIMEType converts the passed-in manifest list to a manifest +// list of the specified type. +func (list *Schema2ListPublic) ConvertToMIMEType(manifestMIMEType string) (ListPublic, error) { + switch normalized := NormalizedMIMEType(manifestMIMEType); normalized { + case DockerV2ListMediaType: + return list.Clone(), nil + case imgspecv1.MediaTypeImageIndex: + return list.ToOCI1Index() + case DockerV2Schema1MediaType, DockerV2Schema1SignedMediaType, imgspecv1.MediaTypeImageManifest, DockerV2Schema2MediaType: + return nil, fmt.Errorf("Can not convert manifest list to MIME type %q, which is not a list type", manifestMIMEType) + default: + // Note that this may not be reachable, NormalizedMIMEType has a default for unknown values. + return nil, fmt.Errorf("Unimplemented manifest list MIME type %s", manifestMIMEType) + } +} + +// Schema2List is a list of platform-specific manifests. +type Schema2List struct { + Schema2ListPublic +} + +func schema2ListFromPublic(public *Schema2ListPublic) *Schema2List { + return &Schema2List{*public} +} + +func (index *Schema2List) CloneInternal() List { + return schema2ListFromPublic(Schema2ListPublicClone(&index.Schema2ListPublic)) +} + +func (index *Schema2List) Clone() ListPublic { + return index.CloneInternal() +} + +// Schema2ListFromManifest creates a Schema2 manifest list instance from marshalled +// JSON, presumably generated by encoding a Schema2 manifest list. +func Schema2ListFromManifest(manifest []byte) (*Schema2List, error) { + public, err := Schema2ListPublicFromManifest(manifest) + if err != nil { + return nil, err + } + return schema2ListFromPublic(public), nil +} diff --git a/internal/manifest/docker_schema2_list_test.go b/internal/manifest/docker_schema2_list_test.go new file mode 100644 index 0000000000..17c243ae38 --- /dev/null +++ b/internal/manifest/docker_schema2_list_test.go @@ -0,0 +1,47 @@ +package manifest + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestSchema2ListPublicFromManifest(t *testing.T) { + validManifest, err := os.ReadFile(filepath.Join("testdata", "v2list.manifest.json")) + require.NoError(t, err) + + parser := func(m []byte) error { + _, err := Schema2ListPublicFromManifest(m) + return err + } + // Schema mismatch is rejected + testManifestFixturesAreRejected(t, parser, []string{ + "schema2-to-schema1-by-docker.json", + "v2s2.manifest.json", + "ociv1.manifest.json", + // Not "ociv1.image.index.json" yet, without validating mediaType the two are too similar to tell the difference. + }) + // Extra fields are rejected + testValidManifestWithExtraFieldsIsRejected(t, parser, validManifest, []string{"config", "fsLayers", "history", "layers"}) +} + +func TestSchema2ListFromManifest(t *testing.T) { + validManifest, err := os.ReadFile(filepath.Join("testdata", "v2list.manifest.json")) + require.NoError(t, err) + + parser := func(m []byte) error { + _, err := Schema2ListFromManifest(m) + return err + } + // Schema mismatch is rejected + testManifestFixturesAreRejected(t, parser, []string{ + "schema2-to-schema1-by-docker.json", + "v2s2.manifest.json", + "ociv1.manifest.json", + // Not "ociv1.image.index.json" yet, without validating mediaType the two are too similar to tell the difference. + }) + // Extra fields are rejected + testValidManifestWithExtraFieldsIsRejected(t, parser, validManifest, []string{"config", "fsLayers", "history", "layers"}) +} diff --git a/internal/manifest/errors.go b/internal/manifest/errors.go index e5732a8c43..6ebe4b24cf 100644 --- a/internal/manifest/errors.go +++ b/internal/manifest/errors.go @@ -2,6 +2,10 @@ package manifest import "fmt" +// FIXME: This is a duplicate of c/image/manifestDockerV2Schema2ConfigMediaType. +// Deduplicate that, depending on outcome of https://github.com/containers/image/pull/1791 . +const dockerV2Schema2ConfigMediaType = "application/vnd.docker.container.image.v1+json" + // NonImageArtifactError (detected via errors.As) is used when asking for an image-specific operation // on an object which is not a “container image” in the standard sense (e.g. an OCI artifact) // @@ -28,5 +32,9 @@ func NewNonImageArtifactError(mimeType string) error { } func (e NonImageArtifactError) Error() string { + // Special-case these invalid mixed images, which show up from time to time: + if e.mimeType == dockerV2Schema2ConfigMediaType { + return fmt.Sprintf("invalid mixed OCI image with Docker v2s2 config (%q)", e.mimeType) + } return fmt.Sprintf("unsupported image-specific operation on artifact with type %q", e.mimeType) } diff --git a/internal/manifest/list.go b/internal/manifest/list.go new file mode 100644 index 0000000000..6866ab01ca --- /dev/null +++ b/internal/manifest/list.go @@ -0,0 +1,86 @@ +package manifest + +import ( + "fmt" + + "github.com/containers/image/v5/types" + digest "github.com/opencontainers/go-digest" + imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" +) + +// ListPublic is a subset of List which is a part of the public API; +// so no methods can be added, removed or changed. +// +// Internal users should usually use List instead. +type ListPublic interface { + // MIMEType returns the MIME type of this particular manifest list. + MIMEType() string + + // Instances returns a list of the manifests that this list knows of, other than its own. + Instances() []digest.Digest + + // Update information about the list's instances. The length of the passed-in slice must + // match the length of the list of instances which the list already contains, and every field + // must be specified. + UpdateInstances([]ListUpdate) error + + // Instance returns the size and MIME type of a particular instance in the list. + Instance(digest.Digest) (ListUpdate, error) + + // ChooseInstance selects which manifest is most appropriate for the platform described by the + // SystemContext, or for the current platform if the SystemContext doesn't specify any details. + ChooseInstance(ctx *types.SystemContext) (digest.Digest, error) + + // Serialize returns the list in a blob format. + // NOTE: Serialize() does not in general reproduce the original blob if this object was loaded + // from, even if no modifications were made! + Serialize() ([]byte, error) + + // ConvertToMIMEType returns the list rebuilt to the specified MIME type, or an error. + ConvertToMIMEType(mimeType string) (ListPublic, error) + + // Clone returns a deep copy of this list and its contents. + Clone() ListPublic +} + +// List is an interface for parsing, modifying lists of image manifests. +// Callers can either use this abstract interface without understanding the details of the formats, +// or instantiate a specific implementation (e.g. manifest.OCI1Index) and access the public members +// directly. +type List interface { + ListPublic + // CloneInternal returns a deep copy of this list and its contents. + CloneInternal() List +} + +// ListUpdate includes the fields which a List's UpdateInstances() method will modify. +// This is publicly visible as c/image/manifest.ListUpdate. +type ListUpdate struct { + Digest digest.Digest + Size int64 + MediaType string +} + +// ListPublicFromBlob parses a list of manifests. +// This is publicly visible as c/image/manifest.ListFromBlob. +func ListPublicFromBlob(manifest []byte, manifestMIMEType string) (ListPublic, error) { + list, err := ListFromBlob(manifest, manifestMIMEType) + if err != nil { + return nil, err + } + return list, nil +} + +// ListFromBlob parses a list of manifests. +func ListFromBlob(manifest []byte, manifestMIMEType string) (List, error) { + normalized := NormalizedMIMEType(manifestMIMEType) + switch normalized { + case DockerV2ListMediaType: + return Schema2ListFromManifest(manifest) + case imgspecv1.MediaTypeImageIndex: + return OCI1IndexFromManifest(manifest) + case DockerV2Schema1MediaType, DockerV2Schema1SignedMediaType, imgspecv1.MediaTypeImageManifest, DockerV2Schema2MediaType: + return nil, fmt.Errorf("Treating single images as manifest lists is not implemented") + } + return nil, fmt.Errorf("Unimplemented manifest list MIME type %s (normalized as %s)", manifestMIMEType, normalized) +} diff --git a/internal/manifest/list_test.go b/internal/manifest/list_test.go new file mode 100644 index 0000000000..f194f355a8 --- /dev/null +++ b/internal/manifest/list_test.go @@ -0,0 +1,139 @@ +package manifest + +import ( + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/containers/image/v5/types" + "github.com/opencontainers/go-digest" + imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func pare(m List) { + if impl, ok := m.(*OCI1Index); ok { + impl.Annotations = nil + } + if impl, ok := m.(*Schema2List); ok { + for i := range impl.Manifests { + impl.Manifests[i].Platform.Features = nil + } + } +} + +func TestParseLists(t *testing.T) { + cases := []struct { + path string + mimeType string + }{ + {"ociv1.image.index.json", imgspecv1.MediaTypeImageIndex}, + {"v2list.manifest.json", DockerV2ListMediaType}, + } + for _, c := range cases { + manifest, err := os.ReadFile(filepath.Join("testdata", c.path)) + require.NoError(t, err, "error reading file %q", filepath.Join("testdata", c.path)) + assert.Equal(t, GuessMIMEType(manifest), c.mimeType) + + // c/image/manifest.TestParseLists verifies that FromBlob refuses to parse the manifest list + + m, err := ListFromBlob(manifest, c.mimeType) + require.NoError(t, err, "manifest list %q should parse as list types", c.path) + assert.Equal(t, m.MIMEType(), c.mimeType, "manifest %q is not of the expected MIME type", c.path) + + clone := m.Clone() + assert.Equal(t, clone, m, "manifest %q is missing some fields after being cloned", c.path) + + pare(m) + + index, err := m.ConvertToMIMEType(imgspecv1.MediaTypeImageIndex) + require.NoError(t, err, "error converting %q to an OCI1Index", c.path) + + list, err := m.ConvertToMIMEType(DockerV2ListMediaType) + require.NoError(t, err, "error converting %q to an Schema2List", c.path) + + index2, err := list.ConvertToMIMEType(imgspecv1.MediaTypeImageIndex) + require.NoError(t, err) + assert.Equal(t, index, index2, "index %q lost data in conversion", c.path) + + list2, err := index.ConvertToMIMEType(DockerV2ListMediaType) + require.NoError(t, err) + assert.Equal(t, list, list2, "list %q lost data in conversion", c.path) + } +} + +func TestChooseInstance(t *testing.T) { + type expectedMatch struct { + arch, variant string + instanceDigest digest.Digest + } + for _, manifestList := range []struct { + listFile string + matchedInstances []expectedMatch + unmatchedInstances []string + }{ + { + listFile: "schema2list.json", + matchedInstances: []expectedMatch{ + {"amd64", "", "sha256:030fcb92e1487b18c974784dcc110a93147c9fc402188370fbfd17efabffc6af"}, + {"s390x", "", "sha256:e5aa1b0a24620228b75382997a0977f609b3ca3a95533dafdef84c74cc8df642"}, + {"arm", "v7", "sha256:b5dbad4bdb4444d919294afe49a095c23e86782f98cdf0aa286198ddb814b50b"}, + {"arm64", "", "sha256:dc472a59fb006797aa2a6bfb54cc9c57959bb0a6d11fadaa608df8c16dea39cf"}, + }, + unmatchedInstances: []string{ + "unmatched", + }, + }, + { // Focus on ARM variant field testing + listFile: "schema2list-variants.json", + matchedInstances: []expectedMatch{ + {"amd64", "", "sha256:59eec8837a4d942cc19a52b8c09ea75121acc38114a2c68b98983ce9356b8610"}, + {"arm", "v7", "sha256:f365626a556e58189fc21d099fc64603db0f440bff07f77c740989515c544a39"}, + {"arm", "v6", "sha256:f365626a556e58189fc21d099fc64603db0f440bff07f77c740989515c544a39"}, + {"arm", "v5", "sha256:c84b0a3a07b628bc4d62e5047d0f8dff80f7c00979e1e28a821a033ecda8fe53"}, + {"arm", "", "sha256:c84b0a3a07b628bc4d62e5047d0f8dff80f7c00979e1e28a821a033ecda8fe53"}, + {"arm", "unrecognized-present", "sha256:bcf9771c0b505e68c65440474179592ffdfa98790eb54ffbf129969c5e429990"}, + {"arm", "unrecognized-not-present", "sha256:c84b0a3a07b628bc4d62e5047d0f8dff80f7c00979e1e28a821a033ecda8fe53"}, + }, + unmatchedInstances: []string{ + "unmatched", + }, + }, + { + listFile: "oci1index.json", + matchedInstances: []expectedMatch{ + {"amd64", "", "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270"}, + {"ppc64le", "", "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f"}, + }, + unmatchedInstances: []string{ + "unmatched", + }, + }, + } { + rawManifest, err := os.ReadFile(filepath.Join("..", "..", "internal", "image", "fixtures", manifestList.listFile)) + require.NoError(t, err) + list, err := ListPublicFromBlob(rawManifest, GuessMIMEType(rawManifest)) + require.NoError(t, err) + // Match found + for _, match := range manifestList.matchedInstances { + testName := fmt.Sprintf("%s %q+%q", manifestList.listFile, match.arch, match.variant) + digest, err := list.ChooseInstance(&types.SystemContext{ + ArchitectureChoice: match.arch, + VariantChoice: match.variant, + OSChoice: "linux", + }) + require.NoError(t, err, testName) + assert.Equal(t, match.instanceDigest, digest, testName) + } + // Not found + for _, arch := range manifestList.unmatchedInstances { + _, err := list.ChooseInstance(&types.SystemContext{ + ArchitectureChoice: arch, + OSChoice: "linux", + }) + assert.Error(t, err) + } + } +} diff --git a/internal/manifest/manifest.go b/internal/manifest/manifest.go new file mode 100644 index 0000000000..1dbcc14182 --- /dev/null +++ b/internal/manifest/manifest.go @@ -0,0 +1,167 @@ +package manifest + +import ( + "encoding/json" + + "github.com/containers/libtrust" + digest "github.com/opencontainers/go-digest" + imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" +) + +// FIXME: Should we just use docker/distribution and docker/docker implementations directly? + +// FIXME(runcom, mitr): should we have a mediatype pkg?? +const ( + // DockerV2Schema1MediaType MIME type represents Docker manifest schema 1 + DockerV2Schema1MediaType = "application/vnd.docker.distribution.manifest.v1+json" + // DockerV2Schema1MediaType MIME type represents Docker manifest schema 1 with a JWS signature + DockerV2Schema1SignedMediaType = "application/vnd.docker.distribution.manifest.v1+prettyjws" + // DockerV2Schema2MediaType MIME type represents Docker manifest schema 2 + DockerV2Schema2MediaType = "application/vnd.docker.distribution.manifest.v2+json" + // DockerV2Schema2ConfigMediaType is the MIME type used for schema 2 config blobs. + DockerV2Schema2ConfigMediaType = "application/vnd.docker.container.image.v1+json" + // DockerV2Schema2LayerMediaType is the MIME type used for schema 2 layers. + DockerV2Schema2LayerMediaType = "application/vnd.docker.image.rootfs.diff.tar.gzip" + // DockerV2SchemaLayerMediaTypeUncompressed is the mediaType used for uncompressed layers. + DockerV2SchemaLayerMediaTypeUncompressed = "application/vnd.docker.image.rootfs.diff.tar" + // DockerV2ListMediaType MIME type represents Docker manifest schema 2 list + DockerV2ListMediaType = "application/vnd.docker.distribution.manifest.list.v2+json" + // DockerV2Schema2ForeignLayerMediaType is the MIME type used for schema 2 foreign layers. + DockerV2Schema2ForeignLayerMediaType = "application/vnd.docker.image.rootfs.foreign.diff.tar" + // DockerV2Schema2ForeignLayerMediaType is the MIME type used for gzipped schema 2 foreign layers. + DockerV2Schema2ForeignLayerMediaTypeGzip = "application/vnd.docker.image.rootfs.foreign.diff.tar.gzip" +) + +// GuessMIMEType guesses MIME type of a manifest and returns it _if it is recognized_, or "" if unknown or unrecognized. +// FIXME? We should, in general, prefer out-of-band MIME type instead of blindly parsing the manifest, +// but we may not have such metadata available (e.g. when the manifest is a local file). +// This is publicly visible as c/image/manifest.GuessMIMEType. +func GuessMIMEType(manifest []byte) string { + // A subset of manifest fields; the rest is silently ignored by json.Unmarshal. + // Also docker/distribution/manifest.Versioned. + meta := struct { + MediaType string `json:"mediaType"` + SchemaVersion int `json:"schemaVersion"` + Signatures any `json:"signatures"` + }{} + if err := json.Unmarshal(manifest, &meta); err != nil { + return "" + } + + switch meta.MediaType { + case DockerV2Schema2MediaType, DockerV2ListMediaType, + imgspecv1.MediaTypeImageManifest, imgspecv1.MediaTypeImageIndex: // A recognized type. + return meta.MediaType + } + // this is the only way the function can return DockerV2Schema1MediaType, and recognizing that is essential for stripping the JWS signatures = computing the correct manifest digest. + switch meta.SchemaVersion { + case 1: + if meta.Signatures != nil { + return DockerV2Schema1SignedMediaType + } + return DockerV2Schema1MediaType + case 2: + // Best effort to understand if this is an OCI image since mediaType + // wasn't in the manifest for OCI image-spec < 1.0.2. + // For docker v2s2 meta.MediaType should have been set. But given the data, this is our best guess. + ociMan := struct { + Config struct { + MediaType string `json:"mediaType"` + } `json:"config"` + }{} + if err := json.Unmarshal(manifest, &ociMan); err != nil { + return "" + } + switch ociMan.Config.MediaType { + case imgspecv1.MediaTypeImageConfig: + return imgspecv1.MediaTypeImageManifest + case DockerV2Schema2ConfigMediaType: + // This case should not happen since a Docker image + // must declare a top-level media type and + // `meta.MediaType` has already been checked. + return DockerV2Schema2MediaType + } + // Maybe an image index or an OCI artifact. + ociIndex := struct { + Manifests []imgspecv1.Descriptor `json:"manifests"` + }{} + if err := json.Unmarshal(manifest, &ociIndex); err != nil { + return "" + } + if len(ociIndex.Manifests) != 0 { + if ociMan.Config.MediaType == "" { + return imgspecv1.MediaTypeImageIndex + } + // FIXME: this is mixing media types of manifests and configs. + return ociMan.Config.MediaType + } + // It's most likely an OCI artifact with a custom config media + // type which is not (and cannot) be covered by the media-type + // checks cabove. + return imgspecv1.MediaTypeImageManifest + } + return "" +} + +// Digest returns the a digest of a docker manifest, with any necessary implied transformations like stripping v1s1 signatures. +// This is publicly visible as c/image/manifest.Digest. +func Digest(manifest []byte) (digest.Digest, error) { + if GuessMIMEType(manifest) == DockerV2Schema1SignedMediaType { + sig, err := libtrust.ParsePrettySignature(manifest, "signatures") + if err != nil { + return "", err + } + manifest, err = sig.Payload() + if err != nil { + // Coverage: This should never happen, libtrust's Payload() can fail only if joseBase64UrlDecode() fails, on a string + // that libtrust itself has josebase64UrlEncode()d + return "", err + } + } + + return digest.FromBytes(manifest), nil +} + +// MatchesDigest returns true iff the manifest matches expectedDigest. +// Error may be set if this returns false. +// Note that this is not doing ConstantTimeCompare; by the time we get here, the cryptographic signature must already have been verified, +// or we are not using a cryptographic channel and the attacker can modify the digest along with the manifest blob. +// This is publicly visible as c/image/manifest.MatchesDigest. +func MatchesDigest(manifest []byte, expectedDigest digest.Digest) (bool, error) { + // This should eventually support various digest types. + actualDigest, err := Digest(manifest) + if err != nil { + return false, err + } + return expectedDigest == actualDigest, nil +} + +// NormalizedMIMEType returns the effective MIME type of a manifest MIME type returned by a server, +// centralizing various workarounds. +// This is publicly visible as c/image/manifest.NormalizedMIMEType. +func NormalizedMIMEType(input string) string { + switch input { + // "application/json" is a valid v2s1 value per https://github.com/docker/distribution/blob/master/docs/spec/manifest-v2-1.md . + // This works for now, when nothing else seems to return "application/json"; if that were not true, the mapping/detection might + // need to happen within the ImageSource. + case "application/json": + return DockerV2Schema1SignedMediaType + case DockerV2Schema1MediaType, DockerV2Schema1SignedMediaType, + imgspecv1.MediaTypeImageManifest, + imgspecv1.MediaTypeImageIndex, + DockerV2Schema2MediaType, + DockerV2ListMediaType: + return input + default: + // If it's not a recognized manifest media type, or we have failed determining the type, we'll try one last time + // to deserialize using v2s1 as per https://github.com/docker/distribution/blob/master/manifests.go#L108 + // and https://github.com/docker/distribution/blob/master/manifest/schema1/manifest.go#L50 + // + // Crane registries can also return "text/plain", or pretty much anything else depending on a file extension “recognized” in the tag. + // This makes no real sense, but it happens + // because requests for manifests are + // redirected to a content distribution + // network which is configured that way. See https://bugzilla.redhat.com/show_bug.cgi?id=1389442 + return DockerV2Schema1SignedMediaType + } +} diff --git a/internal/manifest/manifest_test.go b/internal/manifest/manifest_test.go new file mode 100644 index 0000000000..8dc9879192 --- /dev/null +++ b/internal/manifest/manifest_test.go @@ -0,0 +1,134 @@ +package manifest + +import ( + "os" + "path/filepath" + "testing" + + digest "github.com/opencontainers/go-digest" + imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const ( + digestSha256EmptyTar = "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" +) + +func TestGuessMIMEType(t *testing.T) { + cases := []struct { + path string + mimeType string + }{ + {"v2s2.manifest.json", DockerV2Schema2MediaType}, + {"v2list.manifest.json", DockerV2ListMediaType}, + {"v2s1.manifest.json", DockerV2Schema1SignedMediaType}, + {"v2s1-unsigned.manifest.json", DockerV2Schema1MediaType}, + {"v2s1-invalid-signatures.manifest.json", DockerV2Schema1SignedMediaType}, + {"v2s2nomime.manifest.json", DockerV2Schema2MediaType}, // It is unclear whether this one is legal, but we should guess v2s2 if anything at all. + {"unknown-version.manifest.json", ""}, + {"non-json.manifest.json", ""}, // Not a manifest (nor JSON) at all + {"ociv1.manifest.json", imgspecv1.MediaTypeImageManifest}, + {"ociv1.artifact.json", imgspecv1.MediaTypeImageManifest}, + {"ociv1.image.index.json", imgspecv1.MediaTypeImageIndex}, + {"ociv1nomime.manifest.json", imgspecv1.MediaTypeImageManifest}, + {"ociv1nomime.artifact.json", imgspecv1.MediaTypeImageManifest}, + {"ociv1nomime.image.index.json", imgspecv1.MediaTypeImageIndex}, + } + + for _, c := range cases { + manifest, err := os.ReadFile(filepath.Join("testdata", c.path)) + require.NoError(t, err) + mimeType := GuessMIMEType(manifest) + assert.Equal(t, c.mimeType, mimeType, c.path) + } +} + +func TestDigest(t *testing.T) { + cases := []struct { + path string + expectedDigest digest.Digest + }{ + {"v2s2.manifest.json", TestDockerV2S2ManifestDigest}, + {"v2s1.manifest.json", TestDockerV2S1ManifestDigest}, + {"v2s1-unsigned.manifest.json", TestDockerV2S1UnsignedManifestDigest}, + } + for _, c := range cases { + manifest, err := os.ReadFile(filepath.Join("testdata", c.path)) + require.NoError(t, err) + actualDigest, err := Digest(manifest) + require.NoError(t, err) + assert.Equal(t, c.expectedDigest, actualDigest) + } + + manifest, err := os.ReadFile("testdata/v2s1-invalid-signatures.manifest.json") + require.NoError(t, err) + _, err = Digest(manifest) + assert.Error(t, err) + + actualDigest, err := Digest([]byte{}) + require.NoError(t, err) + assert.Equal(t, digest.Digest(digestSha256EmptyTar), actualDigest) +} + +func TestMatchesDigest(t *testing.T) { + cases := []struct { + path string + expectedDigest digest.Digest + result bool + }{ + // Success + {"v2s2.manifest.json", TestDockerV2S2ManifestDigest, true}, + {"v2s1.manifest.json", TestDockerV2S1ManifestDigest, true}, + // No match (switched s1/s2) + {"v2s2.manifest.json", TestDockerV2S1ManifestDigest, false}, + {"v2s1.manifest.json", TestDockerV2S2ManifestDigest, false}, + // Unrecognized algorithm + {"v2s2.manifest.json", digest.Digest("md5:2872f31c5c1f62a694fbd20c1e85257c"), false}, + // Mangled format + {"v2s2.manifest.json", digest.Digest(TestDockerV2S2ManifestDigest.String() + "abc"), false}, + {"v2s2.manifest.json", digest.Digest(TestDockerV2S2ManifestDigest.String()[:20]), false}, + {"v2s2.manifest.json", digest.Digest(""), false}, + } + for _, c := range cases { + manifest, err := os.ReadFile(filepath.Join("testdata", c.path)) + require.NoError(t, err) + res, err := MatchesDigest(manifest, c.expectedDigest) + require.NoError(t, err) + assert.Equal(t, c.result, res) + } + + manifest, err := os.ReadFile("testdata/v2s1-invalid-signatures.manifest.json") + require.NoError(t, err) + // Even a correct SHA256 hash is rejected if we can't strip the JSON signature. + res, err := MatchesDigest(manifest, digest.FromBytes(manifest)) + assert.False(t, res) + assert.Error(t, err) + + res, err = MatchesDigest([]byte{}, digest.Digest(digestSha256EmptyTar)) + assert.True(t, res) + assert.NoError(t, err) +} + +func TestNormalizedMIMEType(t *testing.T) { + for _, c := range []string{ // Valid MIME types, normalized to themselves + DockerV2Schema1MediaType, + DockerV2Schema1SignedMediaType, + DockerV2Schema2MediaType, + DockerV2ListMediaType, + imgspecv1.MediaTypeImageManifest, + imgspecv1.MediaTypeImageIndex, + } { + res := NormalizedMIMEType(c) + assert.Equal(t, c, res, c) + } + for _, c := range []string{ + "application/json", + "text/plain", + "not at all a valid MIME type", + "", + } { + res := NormalizedMIMEType(c) + assert.Equal(t, DockerV2Schema1SignedMediaType, res, c) + } +} diff --git a/internal/manifest/oci_index.go b/internal/manifest/oci_index.go new file mode 100644 index 0000000000..4e8ef62203 --- /dev/null +++ b/internal/manifest/oci_index.go @@ -0,0 +1,265 @@ +package manifest + +import ( + "encoding/json" + "fmt" + "runtime" + + platform "github.com/containers/image/v5/internal/pkg/platform" + "github.com/containers/image/v5/types" + "github.com/opencontainers/go-digest" + imgspec "github.com/opencontainers/image-spec/specs-go" + imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" + "golang.org/x/exp/maps" + "golang.org/x/exp/slices" +) + +// OCI1IndexPublic is just an alias for the OCI index type, but one which we can +// provide methods for. +// This is publicly visible as c/image/manifest.OCI1Index +// Internal users should usually use OCI1Index instead. +type OCI1IndexPublic struct { + imgspecv1.Index +} + +// MIMEType returns the MIME type of this particular manifest index. +func (index *OCI1IndexPublic) MIMEType() string { + return imgspecv1.MediaTypeImageIndex +} + +// Instances returns a slice of digests of the manifests that this index knows of. +func (index *OCI1IndexPublic) Instances() []digest.Digest { + results := make([]digest.Digest, len(index.Manifests)) + for i, m := range index.Manifests { + results[i] = m.Digest + } + return results +} + +// Instance returns the ListUpdate of a particular instance in the index. +func (index *OCI1IndexPublic) Instance(instanceDigest digest.Digest) (ListUpdate, error) { + for _, manifest := range index.Manifests { + if manifest.Digest == instanceDigest { + return ListUpdate{ + Digest: manifest.Digest, + Size: manifest.Size, + MediaType: manifest.MediaType, + }, nil + } + } + return ListUpdate{}, fmt.Errorf("unable to find instance %s in OCI1Index", instanceDigest) +} + +// UpdateInstances updates the sizes, digests, and media types of the manifests +// which the list catalogs. +func (index *OCI1IndexPublic) UpdateInstances(updates []ListUpdate) error { + if len(updates) != len(index.Manifests) { + return fmt.Errorf("incorrect number of update entries passed to OCI1Index.UpdateInstances: expected %d, got %d", len(index.Manifests), len(updates)) + } + for i := range updates { + if err := updates[i].Digest.Validate(); err != nil { + return fmt.Errorf("update %d of %d passed to OCI1Index.UpdateInstances contained an invalid digest: %w", i+1, len(updates), err) + } + index.Manifests[i].Digest = updates[i].Digest + if updates[i].Size < 0 { + return fmt.Errorf("update %d of %d passed to OCI1Index.UpdateInstances had an invalid size (%d)", i+1, len(updates), updates[i].Size) + } + index.Manifests[i].Size = updates[i].Size + if updates[i].MediaType == "" { + return fmt.Errorf("update %d of %d passed to OCI1Index.UpdateInstances had no media type (was %q)", i+1, len(updates), index.Manifests[i].MediaType) + } + index.Manifests[i].MediaType = updates[i].MediaType + } + return nil +} + +// ChooseInstance parses blob as an oci v1 manifest index, and returns the digest +// of the image which is appropriate for the current environment. +func (index *OCI1IndexPublic) ChooseInstance(ctx *types.SystemContext) (digest.Digest, error) { + wantedPlatforms, err := platform.WantedPlatforms(ctx) + if err != nil { + return "", fmt.Errorf("getting platform information %#v: %w", ctx, err) + } + for _, wantedPlatform := range wantedPlatforms { + for _, d := range index.Manifests { + if d.Platform == nil { + continue + } + imagePlatform := imgspecv1.Platform{ + Architecture: d.Platform.Architecture, + OS: d.Platform.OS, + OSVersion: d.Platform.OSVersion, + OSFeatures: slices.Clone(d.Platform.OSFeatures), + Variant: d.Platform.Variant, + } + if platform.MatchesPlatform(imagePlatform, wantedPlatform) { + return d.Digest, nil + } + } + } + + for _, d := range index.Manifests { + if d.Platform == nil { + return d.Digest, nil + } + } + return "", fmt.Errorf("no image found in image index for architecture %s, variant %q, OS %s", wantedPlatforms[0].Architecture, wantedPlatforms[0].Variant, wantedPlatforms[0].OS) +} + +// Serialize returns the index in a blob format. +// NOTE: Serialize() does not in general reproduce the original blob if this object was loaded from one, even if no modifications were made! +func (index *OCI1IndexPublic) Serialize() ([]byte, error) { + buf, err := json.Marshal(index) + if err != nil { + return nil, fmt.Errorf("marshaling OCI1Index %#v: %w", index, err) + } + return buf, nil +} + +// OCI1IndexPublicFromComponents creates an OCI1 image index instance from the +// supplied data. +// This is publicly visible as c/image/manifest.OCI1IndexFromComponents. +func OCI1IndexPublicFromComponents(components []imgspecv1.Descriptor, annotations map[string]string) *OCI1IndexPublic { + index := OCI1IndexPublic{ + imgspecv1.Index{ + Versioned: imgspec.Versioned{SchemaVersion: 2}, + MediaType: imgspecv1.MediaTypeImageIndex, + Manifests: make([]imgspecv1.Descriptor, len(components)), + Annotations: maps.Clone(annotations), + }, + } + for i, component := range components { + var platform *imgspecv1.Platform + if component.Platform != nil { + platform = &imgspecv1.Platform{ + Architecture: component.Platform.Architecture, + OS: component.Platform.OS, + OSVersion: component.Platform.OSVersion, + OSFeatures: slices.Clone(component.Platform.OSFeatures), + Variant: component.Platform.Variant, + } + } + m := imgspecv1.Descriptor{ + MediaType: component.MediaType, + Size: component.Size, + Digest: component.Digest, + URLs: slices.Clone(component.URLs), + Annotations: maps.Clone(component.Annotations), + Platform: platform, + } + index.Manifests[i] = m + } + return &index +} + +// OCI1IndexPublicClone creates a deep copy of the passed-in index. +// This is publicly visible as c/image/manifest.OCI1IndexClone. +func OCI1IndexPublicClone(index *OCI1IndexPublic) *OCI1IndexPublic { + return OCI1IndexPublicFromComponents(index.Manifests, index.Annotations) +} + +// ToOCI1Index returns the index encoded as an OCI1 index. +func (index *OCI1IndexPublic) ToOCI1Index() (*OCI1IndexPublic, error) { + return OCI1IndexPublicClone(index), nil +} + +// ToSchema2List returns the index encoded as a Schema2 list. +func (index *OCI1IndexPublic) ToSchema2List() (*Schema2ListPublic, error) { + components := make([]Schema2ManifestDescriptor, 0, len(index.Manifests)) + for _, manifest := range index.Manifests { + platform := manifest.Platform + if platform == nil { + platform = &imgspecv1.Platform{ + OS: runtime.GOOS, + Architecture: runtime.GOARCH, + } + } + converted := Schema2ManifestDescriptor{ + Schema2Descriptor{ + MediaType: manifest.MediaType, + Size: manifest.Size, + Digest: manifest.Digest, + URLs: slices.Clone(manifest.URLs), + }, + Schema2PlatformSpec{ + OS: platform.OS, + Architecture: platform.Architecture, + OSFeatures: slices.Clone(platform.OSFeatures), + OSVersion: platform.OSVersion, + Variant: platform.Variant, + }, + } + components = append(components, converted) + } + s2 := Schema2ListPublicFromComponents(components) + return s2, nil +} + +// OCI1IndexPublicFromManifest creates an OCI1 manifest index instance from marshalled +// JSON, presumably generated by encoding a OCI1 manifest index. +// This is publicly visible as c/image/manifest.OCI1IndexFromManifest. +func OCI1IndexPublicFromManifest(manifest []byte) (*OCI1IndexPublic, error) { + index := OCI1IndexPublic{ + Index: imgspecv1.Index{ + Versioned: imgspec.Versioned{SchemaVersion: 2}, + MediaType: imgspecv1.MediaTypeImageIndex, + Manifests: []imgspecv1.Descriptor{}, + Annotations: make(map[string]string), + }, + } + if err := json.Unmarshal(manifest, &index); err != nil { + return nil, fmt.Errorf("unmarshaling OCI1Index %q: %w", string(manifest), err) + } + if err := ValidateUnambiguousManifestFormat(manifest, imgspecv1.MediaTypeImageIndex, + AllowedFieldManifests); err != nil { + return nil, err + } + return &index, nil +} + +// Clone returns a deep copy of this list and its contents. +func (index *OCI1IndexPublic) Clone() ListPublic { + return OCI1IndexPublicClone(index) +} + +// ConvertToMIMEType converts the passed-in image index to a manifest list of +// the specified type. +func (index *OCI1IndexPublic) ConvertToMIMEType(manifestMIMEType string) (ListPublic, error) { + switch normalized := NormalizedMIMEType(manifestMIMEType); normalized { + case DockerV2ListMediaType: + return index.ToSchema2List() + case imgspecv1.MediaTypeImageIndex: + return index.Clone(), nil + case DockerV2Schema1MediaType, DockerV2Schema1SignedMediaType, imgspecv1.MediaTypeImageManifest, DockerV2Schema2MediaType: + return nil, fmt.Errorf("Can not convert image index to MIME type %q, which is not a list type", manifestMIMEType) + default: + // Note that this may not be reachable, NormalizedMIMEType has a default for unknown values. + return nil, fmt.Errorf("Unimplemented manifest MIME type %s", manifestMIMEType) + } +} + +type OCI1Index struct { + OCI1IndexPublic +} + +func oci1IndexFromPublic(public *OCI1IndexPublic) *OCI1Index { + return &OCI1Index{*public} +} + +func (index *OCI1Index) CloneInternal() List { + return oci1IndexFromPublic(OCI1IndexPublicClone(&index.OCI1IndexPublic)) +} + +func (index *OCI1Index) Clone() ListPublic { + return index.CloneInternal() +} + +// OCI1IndexFromManifest creates a OCI1 manifest list instance from marshalled +// JSON, presumably generated by encoding a OCI1 manifest list. +func OCI1IndexFromManifest(manifest []byte) (*OCI1Index, error) { + public, err := OCI1IndexPublicFromManifest(manifest) + if err != nil { + return nil, err + } + return oci1IndexFromPublic(public), nil +} diff --git a/internal/manifest/oci_index_test.go b/internal/manifest/oci_index_test.go new file mode 100644 index 0000000000..5328f887f2 --- /dev/null +++ b/internal/manifest/oci_index_test.go @@ -0,0 +1,47 @@ +package manifest + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestOCI1IndexPublicFromManifest(t *testing.T) { + validManifest, err := os.ReadFile(filepath.Join("testdata", "ociv1.image.index.json")) + require.NoError(t, err) + + parser := func(m []byte) error { + _, err := OCI1IndexPublicFromManifest(m) + return err + } + // Schema mismatch is rejected + testManifestFixturesAreRejected(t, parser, []string{ + "schema2-to-schema1-by-docker.json", + "v2s2.manifest.json", + // Not "v2list.manifest.json" yet, without mediaType the two are too similar to tell the difference. + "ociv1.manifest.json", + }) + // Extra fields are rejected + testValidManifestWithExtraFieldsIsRejected(t, parser, validManifest, []string{"config", "fsLayers", "history", "layers"}) +} + +func TestOCI1IndexFromManifest(t *testing.T) { + validManifest, err := os.ReadFile(filepath.Join("testdata", "ociv1.image.index.json")) + require.NoError(t, err) + + parser := func(m []byte) error { + _, err := OCI1IndexFromManifest(m) + return err + } + // Schema mismatch is rejected + testManifestFixturesAreRejected(t, parser, []string{ + "schema2-to-schema1-by-docker.json", + "v2s2.manifest.json", + // Not "v2list.manifest.json" yet, without mediaType the two are too similar to tell the difference. + "ociv1.manifest.json", + }) + // Extra fields are rejected + testValidManifestWithExtraFieldsIsRejected(t, parser, validManifest, []string{"config", "fsLayers", "history", "layers"}) +} diff --git a/internal/manifest/testdata/non-json.manifest.json b/internal/manifest/testdata/non-json.manifest.json new file mode 100644 index 0000000000..f892721275 Binary files /dev/null and b/internal/manifest/testdata/non-json.manifest.json differ diff --git a/internal/manifest/testdata/ociv1.artifact.json b/internal/manifest/testdata/ociv1.artifact.json new file mode 100644 index 0000000000..a53807924e --- /dev/null +++ b/internal/manifest/testdata/ociv1.artifact.json @@ -0,0 +1,10 @@ +{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "config": { + "mediaType": "application/vnd.oci.custom.artifact.config.v1+json", + "digest": "", + "size": 0 + }, + "layers": null +} diff --git a/internal/manifest/testdata/ociv1.image.index.json b/internal/manifest/testdata/ociv1.image.index.json new file mode 100644 index 0000000000..a85b4d889c --- /dev/null +++ b/internal/manifest/testdata/ociv1.image.index.json @@ -0,0 +1,31 @@ +{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.index.v1+json", + "manifests": [ + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "size": 7143, + "digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f", + "platform": { + "architecture": "ppc64le", + "os": "linux" + } + }, + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "size": 7682, + "digest": "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270", + "platform": { + "architecture": "amd64", + "os": "linux", + "os.features": [ + "sse4" + ] + } + } + ], + "annotations": { + "com.example.key1": "value1", + "com.example.key2": "value2" + } +} diff --git a/internal/manifest/testdata/ociv1.manifest.json b/internal/manifest/testdata/ociv1.manifest.json new file mode 100644 index 0000000000..7e2e2e8274 --- /dev/null +++ b/internal/manifest/testdata/ociv1.manifest.json @@ -0,0 +1,30 @@ +{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "config": { + "mediaType": "application/vnd.oci.image.config.v1+json", + "size": 7023, + "digest": "sha256:b5b2b2c507a0944348e0303114d8d93aaaa081732b86451d9bce1f432a537bc7" + }, + "layers": [ + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "size": 32654, + "digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f" + }, + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "size": 16724, + "digest": "sha256:3c3a4604a545cdc127456d94e421cd355bca5b528f4a9c1905b15da2eb4a4c6b" + }, + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "size": 73109, + "digest": "sha256:ec4b8955958665577945c89419d1af06b5f7636b4ac3da7f12184802ad867736" + } + ], + "annotations": { + "com.example.key1": "value1", + "com.example.key2": "value2" + } +} diff --git a/internal/manifest/testdata/ociv1nomime.artifact.json b/internal/manifest/testdata/ociv1nomime.artifact.json new file mode 100644 index 0000000000..5091d1f61e --- /dev/null +++ b/internal/manifest/testdata/ociv1nomime.artifact.json @@ -0,0 +1,9 @@ +{ + "schemaVersion": 2, + "config": { + "mediaType": "application/vnd.oci.custom.artifact.config.v1+json", + "digest": "", + "size": 0 + }, + "layers": null +} diff --git a/internal/manifest/testdata/ociv1nomime.image.index.json b/internal/manifest/testdata/ociv1nomime.image.index.json new file mode 100644 index 0000000000..066f058db1 --- /dev/null +++ b/internal/manifest/testdata/ociv1nomime.image.index.json @@ -0,0 +1,30 @@ +{ + "schemaVersion": 2, + "manifests": [ + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "size": 7143, + "digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f", + "platform": { + "architecture": "ppc64le", + "os": "linux" + } + }, + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "size": 7682, + "digest": "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270", + "platform": { + "architecture": "amd64", + "os": "linux", + "os.features": [ + "sse4" + ] + } + } + ], + "annotations": { + "com.example.key1": "value1", + "com.example.key2": "value2" + } +} diff --git a/internal/manifest/testdata/ociv1nomime.manifest.json b/internal/manifest/testdata/ociv1nomime.manifest.json new file mode 100644 index 0000000000..1e1047ca7f --- /dev/null +++ b/internal/manifest/testdata/ociv1nomime.manifest.json @@ -0,0 +1,29 @@ +{ + "schemaVersion": 2, + "config": { + "mediaType": "application/vnd.oci.image.config.v1+json", + "size": 7023, + "digest": "sha256:b5b2b2c507a0944348e0303114d8d93aaaa081732b86451d9bce1f432a537bc7" + }, + "layers": [ + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "size": 32654, + "digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f" + }, + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "size": 16724, + "digest": "sha256:3c3a4604a545cdc127456d94e421cd355bca5b528f4a9c1905b15da2eb4a4c6b" + }, + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "size": 73109, + "digest": "sha256:ec4b8955958665577945c89419d1af06b5f7636b4ac3da7f12184802ad867736" + } + ], + "annotations": { + "com.example.key1": "value1", + "com.example.key2": "value2" + } +} diff --git a/internal/manifest/testdata/schema2-to-schema1-by-docker.json b/internal/manifest/testdata/schema2-to-schema1-by-docker.json new file mode 120000 index 0000000000..437a6be334 --- /dev/null +++ b/internal/manifest/testdata/schema2-to-schema1-by-docker.json @@ -0,0 +1 @@ +../../../internal/image/fixtures/schema2-to-schema1-by-docker.json \ No newline at end of file diff --git a/internal/manifest/testdata/unknown-version.manifest.json b/internal/manifest/testdata/unknown-version.manifest.json new file mode 100644 index 0000000000..b0f34b631c --- /dev/null +++ b/internal/manifest/testdata/unknown-version.manifest.json @@ -0,0 +1,5 @@ +{ + "schemaVersion": 99999, + "name": "mitr/noversion-nonsense", + "tag": "latest" +} diff --git a/internal/manifest/testdata/v2list.manifest.json b/internal/manifest/testdata/v2list.manifest.json new file mode 100644 index 0000000000..1bf9896e04 --- /dev/null +++ b/internal/manifest/testdata/v2list.manifest.json @@ -0,0 +1,56 @@ +{ + "schemaVersion": 2, + "mediaType": "application/vnd.docker.distribution.manifest.list.v2+json", + "manifests": [ + { + "mediaType": "application/vnd.docker.distribution.manifest.v1+json", + "size": 2094, + "digest": "sha256:7820f9a86d4ad15a2c4f0c0e5479298df2aa7c2f6871288e2ef8546f3e7b6783", + "platform": { + "architecture": "ppc64le", + "os": "linux" + } + }, + { + "mediaType": "application/vnd.docker.distribution.manifest.v1+json", + "size": 1922, + "digest": "sha256:ae1b0e06e8ade3a11267564a26e750585ba2259c0ecab59ab165ad1af41d1bdd", + "platform": { + "architecture": "amd64", + "os": "linux", + "features": [ + "sse" + ] + } + }, + { + "mediaType": "application/vnd.docker.distribution.manifest.v1+json", + "size": 2084, + "digest": "sha256:e4c0df75810b953d6717b8f8f28298d73870e8aa2a0d5e77b8391f16fdfbbbe2", + "platform": { + "architecture": "s390x", + "os": "linux" + } + }, + { + "mediaType": "application/vnd.docker.distribution.manifest.v1+json", + "size": 2084, + "digest": "sha256:07ebe243465ef4a667b78154ae6c3ea46fdb1582936aac3ac899ea311a701b40", + "platform": { + "architecture": "arm", + "os": "linux", + "variant": "armv7" + } + }, + { + "mediaType": "application/vnd.docker.distribution.manifest.v1+json", + "size": 2090, + "digest": "sha256:fb2fc0707b86dafa9959fe3d29e66af8787aee4d9a23581714be65db4265ad8a", + "platform": { + "architecture": "arm64", + "os": "linux", + "variant": "armv8" + } + } + ] +} diff --git a/internal/manifest/testdata/v2s1-invalid-signatures.manifest.json b/internal/manifest/testdata/v2s1-invalid-signatures.manifest.json new file mode 100644 index 0000000000..96def40232 --- /dev/null +++ b/internal/manifest/testdata/v2s1-invalid-signatures.manifest.json @@ -0,0 +1,11 @@ +{ + "schemaVersion": 1, + "name": "mitr/busybox", + "tag": "latest", + "architecture": "amd64", + "fsLayers": [ + ], + "history": [ + ], + "signatures": 1 +} diff --git a/internal/manifest/testdata/v2s1-unsigned.manifest.json b/internal/manifest/testdata/v2s1-unsigned.manifest.json new file mode 100644 index 0000000000..16764b409e --- /dev/null +++ b/internal/manifest/testdata/v2s1-unsigned.manifest.json @@ -0,0 +1,28 @@ +{ + "schemaVersion": 1, + "name": "mitr/busybox", + "tag": "latest", + "architecture": "amd64", + "fsLayers": [ + { + "blobSum": "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef" + }, + { + "blobSum": "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef" + }, + { + "blobSum": "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef" + } + ], + "history": [ + { + "v1Compatibility": "{\"id\":\"f1b5eb0a1215f663765d509b6cdf3841bc2bcff0922346abb943d1342d469a97\",\"parent\":\"594075be8d003f784074cc639d970d1fa091a8197850baaae5052c01564ac535\",\"created\":\"2016-03-03T11:29:44.222098366Z\",\"container\":\"c0924f5b281a1992127d0afc065e59548ded8880b08aea4debd56d4497acb17a\",\"container_config\":{\"Hostname\":\"56f0fe1dfc95\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":null,\"PublishService\":\"\",\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) LABEL Checksum=4fef81d30f31f9213c642881357e6662846a0f884c2366c13ebad807b4031368 ./tests/test-images/Dockerfile.2\"],\"Image\":\"594075be8d003f784074cc639d970d1fa091a8197850baaae5052c01564ac535\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":{\"Checksum\":\"4fef81d30f31f9213c642881357e6662846a0f884c2366c13ebad807b4031368 ./tests/test-images/Dockerfile.2\",\"Name\":\"atomic-test-2\"}},\"docker_version\":\"1.8.2-fc22\",\"author\":\"\\\"William Temple \\u003cwtemple at redhat dot com\\u003e\\\"\",\"config\":{\"Hostname\":\"56f0fe1dfc95\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":null,\"PublishService\":\"\",\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":null,\"Image\":\"594075be8d003f784074cc639d970d1fa091a8197850baaae5052c01564ac535\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":{\"Checksum\":\"4fef81d30f31f9213c642881357e6662846a0f884c2366c13ebad807b4031368 ./tests/test-images/Dockerfile.2\",\"Name\":\"atomic-test-2\"}},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":0}\n" + }, + { + "v1Compatibility": "{\"id\":\"594075be8d003f784074cc639d970d1fa091a8197850baaae5052c01564ac535\",\"parent\":\"03dfa1cd1abe452bc2b69b8eb2362fa6beebc20893e65437906318954f6276d4\",\"created\":\"2016-03-03T11:29:38.563048924Z\",\"container\":\"fd4cf54dcd239fbae9bdade9db48e41880b436d27cb5313f60952a46ab04deff\",\"container_config\":{\"Hostname\":\"56f0fe1dfc95\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":null,\"PublishService\":\"\",\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) LABEL Name=atomic-test-2\"],\"Image\":\"03dfa1cd1abe452bc2b69b8eb2362fa6beebc20893e65437906318954f6276d4\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":{\"Name\":\"atomic-test-2\"}},\"docker_version\":\"1.8.2-fc22\",\"author\":\"\\\"William Temple \\u003cwtemple at redhat dot com\\u003e\\\"\",\"config\":{\"Hostname\":\"56f0fe1dfc95\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":null,\"PublishService\":\"\",\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":null,\"Image\":\"03dfa1cd1abe452bc2b69b8eb2362fa6beebc20893e65437906318954f6276d4\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":{\"Name\":\"atomic-test-2\"}},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":0}\n" + }, + { + "v1Compatibility": "{\"id\":\"03dfa1cd1abe452bc2b69b8eb2362fa6beebc20893e65437906318954f6276d4\",\"created\":\"2016-03-03T11:29:32.948089874Z\",\"container\":\"56f0fe1dfc95755dd6cda10f7215c9937a8d9c6348d079c581a261fd4c2f3a5f\",\"container_config\":{\"Hostname\":\"56f0fe1dfc95\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":null,\"PublishService\":\"\",\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) MAINTAINER \\\"William Temple \\u003cwtemple at redhat dot com\\u003e\\\"\"],\"Image\":\"\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":null},\"docker_version\":\"1.8.2-fc22\",\"author\":\"\\\"William Temple \\u003cwtemple at redhat dot com\\u003e\\\"\",\"config\":{\"Hostname\":\"56f0fe1dfc95\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":null,\"PublishService\":\"\",\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":null,\"Image\":\"\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":null},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":0}\n" + } + ] +} \ No newline at end of file diff --git a/internal/manifest/testdata/v2s1.manifest.json b/internal/manifest/testdata/v2s1.manifest.json new file mode 100644 index 0000000000..f7bcd07401 --- /dev/null +++ b/internal/manifest/testdata/v2s1.manifest.json @@ -0,0 +1,44 @@ +{ + "schemaVersion": 1, + "name": "mitr/busybox", + "tag": "latest", + "architecture": "amd64", + "fsLayers": [ + { + "blobSum": "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef" + }, + { + "blobSum": "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef" + }, + { + "blobSum": "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef" + } + ], + "history": [ + { + "v1Compatibility": "{\"id\":\"f1b5eb0a1215f663765d509b6cdf3841bc2bcff0922346abb943d1342d469a97\",\"parent\":\"594075be8d003f784074cc639d970d1fa091a8197850baaae5052c01564ac535\",\"created\":\"2016-03-03T11:29:44.222098366Z\",\"container\":\"c0924f5b281a1992127d0afc065e59548ded8880b08aea4debd56d4497acb17a\",\"container_config\":{\"Hostname\":\"56f0fe1dfc95\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":null,\"PublishService\":\"\",\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) LABEL Checksum=4fef81d30f31f9213c642881357e6662846a0f884c2366c13ebad807b4031368 ./tests/test-images/Dockerfile.2\"],\"Image\":\"594075be8d003f784074cc639d970d1fa091a8197850baaae5052c01564ac535\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":{\"Checksum\":\"4fef81d30f31f9213c642881357e6662846a0f884c2366c13ebad807b4031368 ./tests/test-images/Dockerfile.2\",\"Name\":\"atomic-test-2\"}},\"docker_version\":\"1.8.2-fc22\",\"author\":\"\\\"William Temple \\u003cwtemple at redhat dot com\\u003e\\\"\",\"config\":{\"Hostname\":\"56f0fe1dfc95\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":null,\"PublishService\":\"\",\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":null,\"Image\":\"594075be8d003f784074cc639d970d1fa091a8197850baaae5052c01564ac535\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":{\"Checksum\":\"4fef81d30f31f9213c642881357e6662846a0f884c2366c13ebad807b4031368 ./tests/test-images/Dockerfile.2\",\"Name\":\"atomic-test-2\"}},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":0}\n" + }, + { + "v1Compatibility": "{\"id\":\"594075be8d003f784074cc639d970d1fa091a8197850baaae5052c01564ac535\",\"parent\":\"03dfa1cd1abe452bc2b69b8eb2362fa6beebc20893e65437906318954f6276d4\",\"created\":\"2016-03-03T11:29:38.563048924Z\",\"container\":\"fd4cf54dcd239fbae9bdade9db48e41880b436d27cb5313f60952a46ab04deff\",\"container_config\":{\"Hostname\":\"56f0fe1dfc95\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":null,\"PublishService\":\"\",\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) LABEL Name=atomic-test-2\"],\"Image\":\"03dfa1cd1abe452bc2b69b8eb2362fa6beebc20893e65437906318954f6276d4\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":{\"Name\":\"atomic-test-2\"}},\"docker_version\":\"1.8.2-fc22\",\"author\":\"\\\"William Temple \\u003cwtemple at redhat dot com\\u003e\\\"\",\"config\":{\"Hostname\":\"56f0fe1dfc95\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":null,\"PublishService\":\"\",\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":null,\"Image\":\"03dfa1cd1abe452bc2b69b8eb2362fa6beebc20893e65437906318954f6276d4\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":{\"Name\":\"atomic-test-2\"}},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":0}\n" + }, + { + "v1Compatibility": "{\"id\":\"03dfa1cd1abe452bc2b69b8eb2362fa6beebc20893e65437906318954f6276d4\",\"created\":\"2016-03-03T11:29:32.948089874Z\",\"container\":\"56f0fe1dfc95755dd6cda10f7215c9937a8d9c6348d079c581a261fd4c2f3a5f\",\"container_config\":{\"Hostname\":\"56f0fe1dfc95\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":null,\"PublishService\":\"\",\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) MAINTAINER \\\"William Temple \\u003cwtemple at redhat dot com\\u003e\\\"\"],\"Image\":\"\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":null},\"docker_version\":\"1.8.2-fc22\",\"author\":\"\\\"William Temple \\u003cwtemple at redhat dot com\\u003e\\\"\",\"config\":{\"Hostname\":\"56f0fe1dfc95\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":null,\"PublishService\":\"\",\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":null,\"Image\":\"\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":null},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":0}\n" + } + ], + "signatures": [ + { + "header": { + "jwk": { + "crv": "P-256", + "kid": "OZ45:U3IG:TDOI:PMBD:NGP2:LDIW:II2U:PSBI:MMCZ:YZUP:TUUO:XPZT", + "kty": "EC", + "x": "ReC5c0J9tgXSdUL4_xzEt5RsD8kFt2wWSgJcpAcOQx8", + "y": "3sBGEqQ3ZMeqPKwQBAadN2toOUEASha18xa0WwsDF-M" + }, + "alg": "ES256" + }, + "signature": "dV1paJ3Ck1Ph4FcEhg_frjqxdlGdI6-ywRamk6CvMOcaOEUdCWCpCPQeBQpD2N6tGjkoG1BbstkFNflllfenCw", + "protected": "eyJmb3JtYXRMZW5ndGgiOjU0NzgsImZvcm1hdFRhaWwiOiJDbjAiLCJ0aW1lIjoiMjAxNi0wNC0xOFQyMDo1NDo0MloifQ" + } + ] +} \ No newline at end of file diff --git a/internal/manifest/testdata/v2s2.manifest.json b/internal/manifest/testdata/v2s2.manifest.json new file mode 100644 index 0000000000..198da23f92 --- /dev/null +++ b/internal/manifest/testdata/v2s2.manifest.json @@ -0,0 +1,26 @@ +{ + "schemaVersion": 2, + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "config": { + "mediaType": "application/vnd.docker.container.image.v1+json", + "size": 7023, + "digest": "sha256:b5b2b2c507a0944348e0303114d8d93aaaa081732b86451d9bce1f432a537bc7" + }, + "layers": [ + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "size": 32654, + "digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f" + }, + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "size": 16724, + "digest": "sha256:3c3a4604a545cdc127456d94e421cd355bca5b528f4a9c1905b15da2eb4a4c6b" + }, + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "size": 73109, + "digest": "sha256:ec4b8955958665577945c89419d1af06b5f7636b4ac3da7f12184802ad867736" + } + ] +} \ No newline at end of file diff --git a/internal/manifest/testdata/v2s2nomime.manifest.json b/internal/manifest/testdata/v2s2nomime.manifest.json new file mode 100644 index 0000000000..a0b06c233b --- /dev/null +++ b/internal/manifest/testdata/v2s2nomime.manifest.json @@ -0,0 +1,10 @@ +{ + "schemaVersion": 2, + "config": { + "mediaType": "application/vnd.docker.container.image.v1+json", + "size": 7023, + "digest": "sha256:b5b2b2c507a0944348e0303114d8d93aaaa081732b86451d9bce1f432a537bc7" + }, + "layers": [ + ] +} diff --git a/internal/manifest/testdata_info_test.go b/internal/manifest/testdata_info_test.go new file mode 100644 index 0000000000..bfdaed1a0b --- /dev/null +++ b/internal/manifest/testdata_info_test.go @@ -0,0 +1,12 @@ +package manifest + +import "github.com/opencontainers/go-digest" + +const ( + // TestV2S2ManifestDigest is the Docker manifest digest of "v2s2.manifest.json" + TestDockerV2S2ManifestDigest = digest.Digest("sha256:20bf21ed457b390829cdbeec8795a7bea1626991fda603e0d01b4e7f60427e55") + // TestV2S1ManifestDigest is the Docker manifest digest of "v2s1.manifest.json" + TestDockerV2S1ManifestDigest = digest.Digest("sha256:7364fea9d84ee548ab67d4c46c6006289800c98de3fbf8c0a97138dfcc23f000") + // TestV2S1UnsignedManifestDigest is the Docker manifest digest of "v2s1unsigned.manifest.json" + TestDockerV2S1UnsignedManifestDigest = digest.Digest("sha256:7364fea9d84ee548ab67d4c46c6006289800c98de3fbf8c0a97138dfcc23f000") +) diff --git a/internal/pkg/platform/platform_matcher.go b/internal/pkg/platform/platform_matcher.go index 3bda6af291..59b1d4b9e2 100644 --- a/internal/pkg/platform/platform_matcher.go +++ b/internal/pkg/platform/platform_matcher.go @@ -25,6 +25,7 @@ import ( "github.com/containers/image/v5/types" imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" + "golang.org/x/exp/slices" ) // For Linux, the kernel has already detected the ABI, ISA and Features. @@ -152,13 +153,9 @@ func WantedPlatforms(ctx *types.SystemContext) ([]imgspecv1.Platform, error) { if wantedVariant != "" { // If the user requested a specific variant, we'll walk down // the list from most to least compatible. - if compatibility[wantedArch] != nil { - variantOrder := compatibility[wantedArch] - for i, v := range variantOrder { - if wantedVariant == v { - variants = variantOrder[i:] - break - } + if variantOrder := compatibility[wantedArch]; variantOrder != nil { + if i := slices.Index(variantOrder, wantedVariant); i != -1 { + variants = variantOrder[i:] } } if variants == nil { diff --git a/internal/set/set.go b/internal/set/set.go new file mode 100644 index 0000000000..5c7bcabef8 --- /dev/null +++ b/internal/set/set.go @@ -0,0 +1,46 @@ +package set + +import "golang.org/x/exp/maps" + +// FIXME: +// - Docstrings +// - This should be in a public library somewhere + +type Set[E comparable] struct { + m map[E]struct{} +} + +func New[E comparable]() *Set[E] { + return &Set[E]{ + m: map[E]struct{}{}, + } +} + +func NewWithValues[E comparable](values ...E) *Set[E] { + s := New[E]() + for _, v := range values { + s.Add(v) + } + return s +} + +func (s Set[E]) Add(v E) { + s.m[v] = struct{}{} // Possibly writing the same struct{}{} presence marker again. +} + +func (s Set[E]) Delete(v E) { + delete(s.m, v) +} + +func (s *Set[E]) Contains(v E) bool { + _, ok := s.m[v] + return ok +} + +func (s *Set[E]) Empty() bool { + return len(s.m) == 0 +} + +func (s *Set[E]) Values() []E { + return maps.Keys(s.m) +} diff --git a/internal/signature/signature.go b/internal/signature/signature.go index ee90b788b0..6f95115a13 100644 --- a/internal/signature/signature.go +++ b/internal/signature/signature.go @@ -24,7 +24,7 @@ type Signature interface { blobChunk() ([]byte, error) } -// BlobChunk returns a representation of sig as a []byte, suitable for long-term storage. +// Blob returns a representation of sig as a []byte, suitable for long-term storage. func Blob(sig Signature) ([]byte, error) { chunk, err := sig.blobChunk() if err != nil { @@ -66,22 +66,20 @@ func FromBlob(blob []byte) (Signature, error) { // The newer format: binary 0, format name, newline, data case 0x00: blob = blob[1:] - newline := bytes.IndexByte(blob, '\n') - if newline == -1 { + formatBytes, blobChunk, foundNewline := bytes.Cut(blob, []byte{'\n'}) + if !foundNewline { return nil, fmt.Errorf("invalid signature format, missing newline") } - formatBytes := blob[:newline] for _, b := range formatBytes { if b < 32 || b >= 0x7F { return nil, fmt.Errorf("invalid signature format, non-ASCII byte %#x", b) } } - blobChunk := blob[newline+1:] switch { case bytes.Equal(formatBytes, []byte(SimpleSigningFormat)): return SimpleSigningFromBlob(blobChunk), nil case bytes.Equal(formatBytes, []byte(SigstoreFormat)): - return SigstoreFromBlobChunk(blobChunk) + return sigstoreFromBlobChunk(blobChunk) default: return nil, fmt.Errorf("unrecognized signature format %q", string(formatBytes)) } @@ -102,10 +100,3 @@ func UnsupportedFormatError(sig Signature) error { return fmt.Errorf("unsupported, and unrecognized, signature format %q", string(formatID)) } } - -// copyByteSlice returns a guaranteed-fresh copy of a byte slice -// Use this to make sure the underlying data is not shared and can’t be unexpectedly modified. -func copyByteSlice(s []byte) []byte { - res := []byte{} - return append(res, s...) -} diff --git a/internal/signature/sigstore.go b/internal/signature/sigstore.go index 17342c8b76..b8a9b366cc 100644 --- a/internal/signature/sigstore.go +++ b/internal/signature/sigstore.go @@ -1,12 +1,23 @@ package signature -import "encoding/json" +import ( + "encoding/json" + + "golang.org/x/exp/maps" + "golang.org/x/exp/slices" +) const ( // from sigstore/cosign/pkg/types.SimpleSigningMediaType SigstoreSignatureMIMEType = "application/vnd.dev.cosign.simplesigning.v1+json" // from sigstore/cosign/pkg/oci/static.SignatureAnnotationKey SigstoreSignatureAnnotationKey = "dev.cosignproject.cosign/signature" + // from sigstore/cosign/pkg/oci/static.BundleAnnotationKey + SigstoreSETAnnotationKey = "dev.sigstore.cosign/bundle" + // from sigstore/cosign/pkg/oci/static.CertificateAnnotationKey + SigstoreCertificateAnnotationKey = "dev.sigstore.cosign/certificate" + // from sigstore/cosign/pkg/oci/static.ChainAnnotationKey + SigstoreIntermediateCertificateChainAnnotationKey = "dev.sigstore.cosign/chain" ) // Sigstore is a github.com/cosign/cosign signature. @@ -34,13 +45,13 @@ type sigstoreJSONRepresentation struct { func SigstoreFromComponents(untrustedMimeType string, untrustedPayload []byte, untrustedAnnotations map[string]string) Sigstore { return Sigstore{ untrustedMIMEType: untrustedMimeType, - untrustedPayload: copyByteSlice(untrustedPayload), - untrustedAnnotations: copyStringMap(untrustedAnnotations), + untrustedPayload: slices.Clone(untrustedPayload), + untrustedAnnotations: maps.Clone(untrustedAnnotations), } } -// SigstoreFromBlobChunk converts a Sigstore signature, as returned by Sigstore.blobChunk, into a Sigstore object. -func SigstoreFromBlobChunk(blobChunk []byte) (Sigstore, error) { +// sigstoreFromBlobChunk converts a Sigstore signature, as returned by Sigstore.blobChunk, into a Sigstore object. +func sigstoreFromBlobChunk(blobChunk []byte) (Sigstore, error) { var v sigstoreJSONRepresentation if err := json.Unmarshal(blobChunk, &v); err != nil { return Sigstore{}, err @@ -68,17 +79,9 @@ func (s Sigstore) UntrustedMIMEType() string { return s.untrustedMIMEType } func (s Sigstore) UntrustedPayload() []byte { - return copyByteSlice(s.untrustedPayload) + return slices.Clone(s.untrustedPayload) } func (s Sigstore) UntrustedAnnotations() map[string]string { - return copyStringMap(s.untrustedAnnotations) -} - -func copyStringMap(m map[string]string) map[string]string { - res := map[string]string{} - for k, v := range m { - res[k] = v - } - return res + return maps.Clone(s.untrustedAnnotations) } diff --git a/internal/signature/sigstore_test.go b/internal/signature/sigstore_test.go index 3e34b28991..c1d3c9b90d 100644 --- a/internal/signature/sigstore_test.go +++ b/internal/signature/sigstore_test.go @@ -24,14 +24,14 @@ func TestSigstoreFromComponents(t *testing.T) { func TestSigstoreFromBlobChunk(t *testing.T) { // Success json := []byte(`{"mimeType":"mime-type","payload":"cGF5bG9hZA==", "annotations":{"a":"b","c":"d"}}`) - res, err := SigstoreFromBlobChunk(json) + res, err := sigstoreFromBlobChunk(json) require.NoError(t, err) assert.Equal(t, "mime-type", res.UntrustedMIMEType()) assert.Equal(t, []byte("payload"), res.UntrustedPayload()) assert.Equal(t, map[string]string{"a": "b", "c": "d"}, res.UntrustedAnnotations()) // Invalid JSON - _, err = SigstoreFromBlobChunk([]byte("&")) + _, err = sigstoreFromBlobChunk([]byte("&")) assert.Error(t, err) } @@ -49,7 +49,7 @@ func TestSigstoreBlobChunk(t *testing.T) { expectedJSON := []byte(`{"mimeType":"mime-type","payload":"cGF5bG9hZA==", "annotations":{"a":"b","c":"d"}}`) // Don’t directly compare the JSON representation so that we don’t test for formatting differences, just verify that it contains exactly the expected data. - var raw, expectedRaw map[string]interface{} + var raw, expectedRaw map[string]any err = json.Unmarshal(res, &raw) require.NoError(t, err) err = json.Unmarshal(expectedJSON, &expectedRaw) diff --git a/internal/signature/simple.go b/internal/signature/simple.go index 88b8adad03..c093704060 100644 --- a/internal/signature/simple.go +++ b/internal/signature/simple.go @@ -1,5 +1,7 @@ package signature +import "golang.org/x/exp/slices" + // SimpleSigning is a “simple signing” signature. type SimpleSigning struct { untrustedSignature []byte @@ -8,7 +10,7 @@ type SimpleSigning struct { // SimpleSigningFromBlob converts a “simple signing” signature into a SimpleSigning object. func SimpleSigningFromBlob(blobChunk []byte) SimpleSigning { return SimpleSigning{ - untrustedSignature: copyByteSlice(blobChunk), + untrustedSignature: slices.Clone(blobChunk), } } @@ -19,9 +21,9 @@ func (s SimpleSigning) FormatID() FormatID { // blobChunk returns a representation of signature as a []byte, suitable for long-term storage. // Almost everyone should use signature.Blob() instead. func (s SimpleSigning) blobChunk() ([]byte, error) { - return copyByteSlice(s.untrustedSignature), nil + return slices.Clone(s.untrustedSignature), nil } func (s SimpleSigning) UntrustedSignature() []byte { - return copyByteSlice(s.untrustedSignature) + return slices.Clone(s.untrustedSignature) } diff --git a/internal/signer/signer.go b/internal/signer/signer.go new file mode 100644 index 0000000000..5720254d1c --- /dev/null +++ b/internal/signer/signer.go @@ -0,0 +1,47 @@ +package signer + +import ( + "context" + + "github.com/containers/image/v5/docker/reference" + "github.com/containers/image/v5/internal/signature" +) + +// Signer is an object, possibly carrying state, that can be used by copy.Image to sign one or more container images. +// This type is visible to external callers, so it has no public fields or methods apart from Close(). +// +// The owner of a Signer must call Close() when done. +type Signer struct { + implementation SignerImplementation +} + +// NewSigner creates a public Signer from a SignerImplementation +func NewSigner(impl SignerImplementation) *Signer { + return &Signer{implementation: impl} +} + +func (s *Signer) Close() error { + return s.implementation.Close() +} + +// ProgressMessage returns a human-readable sentence that makes sense to write before starting to create a single signature. +// Alternatively, should SignImageManifest be provided a logging writer of some kind? +func ProgressMessage(signer *Signer) string { + return signer.implementation.ProgressMessage() +} + +// SignImageManifest invokes a SignerImplementation. +// This is a function, not a method, so that it can only be called by code that is allowed to import this internal subpackage. +func SignImageManifest(ctx context.Context, signer *Signer, manifest []byte, dockerReference reference.Named) (signature.Signature, error) { + return signer.implementation.SignImageManifest(ctx, manifest, dockerReference) +} + +// SignerImplementation is an object, possibly carrying state, that can be used by copy.Image to sign one or more container images. +// This interface is distinct from Signer so that implementations can be created outside of this package. +type SignerImplementation interface { + // ProgressMessage returns a human-readable sentence that makes sense to write before starting to create a single signature. + ProgressMessage() string + // SignImageManifest creates a new signature for manifest m as dockerReference. + SignImageManifest(ctx context.Context, m []byte, dockerReference reference.Named) (signature.Signature, error) + Close() error +} diff --git a/internal/signer/signer_test.go b/internal/signer/signer_test.go new file mode 100644 index 0000000000..eaa18cf8ae --- /dev/null +++ b/internal/signer/signer_test.go @@ -0,0 +1,87 @@ +package signer + +import ( + "context" + "errors" + "testing" + + "github.com/containers/image/v5/docker/reference" + "github.com/containers/image/v5/internal/signature" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// mockSignerImplementation is a SignerImplementation used only for tests. +type mockSignerImplementation struct { + progressMessage func() string + signImageManifest func(ctx context.Context, m []byte, dockerReference reference.Named) (signature.Signature, error) + close func() error +} + +func (ms *mockSignerImplementation) Close() error { + return ms.close() +} + +func (ms *mockSignerImplementation) ProgressMessage() string { + return ms.progressMessage() +} + +func (ms *mockSignerImplementation) SignImageManifest(ctx context.Context, m []byte, dockerReference reference.Named) (signature.Signature, error) { + return ms.signImageManifest(ctx, m, dockerReference) +} + +func TestNewSigner(t *testing.T) { + closeError := errors.New("unique error") + + si := mockSignerImplementation{ + // Other functions are nil, so this ensures they are not called. + close: func() error { return closeError }, + } + s := NewSigner(&si) + // Verify SignerImplementation methods are not visible even to determined callers + _, visible := any(s).(SignerImplementation) + assert.False(t, visible) + err := s.Close() + assert.Equal(t, closeError, err) +} + +func TestProgressMessage(t *testing.T) { + si := mockSignerImplementation{ + // Other functions are nil, so this ensures they are not called. + close: func() error { return nil }, + } + s := NewSigner(&si) + defer s.Close() + + const testMessage = "some unique string" + si.progressMessage = func() string { + return testMessage + } + message := ProgressMessage(s) + assert.Equal(t, testMessage, message) +} + +func TestSignImageManifest(t *testing.T) { + si := mockSignerImplementation{ + // Other functions are nil, so this ensures they are not called. + close: func() error { return nil }, + } + s := NewSigner(&si) + defer s.Close() + + testManifest := []byte("some manifest") + testDR, err := reference.ParseNormalizedNamed("busybox") + require.NoError(t, err) + testContext := context.WithValue(context.Background(), struct{}{}, "make this context unique") + testSig := signature.SigstoreFromComponents(signature.SigstoreSignatureMIMEType, []byte("payload"), nil) + testErr := errors.New("some unique error") + si.signImageManifest = func(ctx context.Context, m []byte, dockerReference reference.Named) (signature.Signature, error) { + assert.Equal(t, testContext, ctx) + assert.Equal(t, testManifest, m) + assert.Equal(t, testDR, dockerReference) + return testSig, testErr + } + sig, err := SignImageManifest(testContext, s, testManifest, testDR) + assert.Equal(t, testSig, sig) + assert.Equal(t, testErr, err) +} diff --git a/internal/testing/gpgagent/gpg_agent.go b/internal/testing/gpgagent/gpg_agent.go new file mode 100644 index 0000000000..3de34eb662 --- /dev/null +++ b/internal/testing/gpgagent/gpg_agent.go @@ -0,0 +1,14 @@ +package gpgagent + +import ( + "os" + "os/exec" +) + +// Kill the running gpg-agent to drop unlocked keys. +// This is useful to ensure tests don’t leave processes around (in TestMain), or for testing handling of invalid passphrases. +func KillGPGAgent(gpgHomeDir string) error { + cmd := exec.Command("gpgconf", "--kill", "gpg-agent") + cmd.Env = append(os.Environ(), "GNUPGHOME="+gpgHomeDir) + return cmd.Run() +} diff --git a/internal/uploadreader/upload_reader.go b/internal/uploadreader/upload_reader.go index 6aa9ead68f..b95370af76 100644 --- a/internal/uploadreader/upload_reader.go +++ b/internal/uploadreader/upload_reader.go @@ -11,7 +11,7 @@ import ( // The net/http package uses a separate goroutine to upload data to a HTTP connection, // and it is possible for the server to return a response (typically an error) before consuming // the full body of the request. In that case http.Client.Do can return with an error while -// the body is still being read — regardless of of the cancellation, if any, of http.Request.Context(). +// the body is still being read — regardless of the cancellation, if any, of http.Request.Context(). // // As a result, any data used/updated by the io.Reader() provided as the request body may be // used/updated even after http.Client.Do returns, causing races. diff --git a/internal/useragent/useragent.go b/internal/useragent/useragent.go new file mode 100644 index 0000000000..7ac49693ed --- /dev/null +++ b/internal/useragent/useragent.go @@ -0,0 +1,6 @@ +package useragent + +import "github.com/containers/image/v5/version" + +// DefaultUserAgent is a value that should be used by User-Agent headers, unless the user specifically instructs us otherwise. +var DefaultUserAgent = "containers/" + version.Version + " (github.com/containers/image)" diff --git a/manifest/common.go b/manifest/common.go index 5f352acc2f..1bdcf3d305 100644 --- a/manifest/common.go +++ b/manifest/common.go @@ -1,7 +1,6 @@ package manifest import ( - "encoding/json" "fmt" compressiontypes "github.com/containers/image/v5/pkg/compression/types" @@ -9,96 +8,6 @@ import ( "github.com/sirupsen/logrus" ) -// dupStringSlice returns a deep copy of a slice of strings, or nil if the -// source slice is empty. -func dupStringSlice(list []string) []string { - if len(list) == 0 { - return nil - } - dup := make([]string, len(list)) - copy(dup, list) - return dup -} - -// dupStringStringMap returns a deep copy of a map[string]string, or nil if the -// passed-in map is nil or has no keys. -func dupStringStringMap(m map[string]string) map[string]string { - if len(m) == 0 { - return nil - } - result := make(map[string]string) - for k, v := range m { - result[k] = v - } - return result -} - -// allowedManifestFields is a bit mask of “essential” manifest fields that validateUnambiguousManifestFormat -// can expect to be present. -type allowedManifestFields int - -const ( - allowedFieldConfig allowedManifestFields = 1 << iota - allowedFieldFSLayers - allowedFieldHistory - allowedFieldLayers - allowedFieldManifests - allowedFieldFirstUnusedBit // Keep this at the end! -) - -// validateUnambiguousManifestFormat rejects manifests (incl. multi-arch) that look like more than -// one kind we currently recognize, i.e. if they contain any of the known “essential” format fields -// other than the ones the caller specifically allows. -// expectedMIMEType is used only for diagnostics. -// NOTE: The caller should do the non-heuristic validations (e.g. check for any specified format -// identification/version, or other “magic numbers”) before calling this, to cleanly reject unambiguous -// data that just isn’t what was expected, as opposed to actually ambiguous data. -func validateUnambiguousManifestFormat(manifest []byte, expectedMIMEType string, - allowed allowedManifestFields) error { - if allowed >= allowedFieldFirstUnusedBit { - return fmt.Errorf("internal error: invalid allowedManifestFields value %#v", allowed) - } - // Use a private type to decode, not just a map[string]interface{}, because we want - // to also reject case-insensitive matches (which would be used by Go when really decoding - // the manifest). - // (It is expected that as manifest formats are added or extended over time, more fields will be added - // here.) - detectedFields := struct { - Config interface{} `json:"config"` - FSLayers interface{} `json:"fsLayers"` - History interface{} `json:"history"` - Layers interface{} `json:"layers"` - Manifests interface{} `json:"manifests"` - }{} - if err := json.Unmarshal(manifest, &detectedFields); err != nil { - // The caller was supposed to already validate version numbers, so this should not happen; - // let’s not bother with making this error “nice”. - return err - } - unexpected := []string{} - // Sadly this isn’t easy to automate in Go, without reflection. So, copy&paste. - if detectedFields.Config != nil && (allowed&allowedFieldConfig) == 0 { - unexpected = append(unexpected, "config") - } - if detectedFields.FSLayers != nil && (allowed&allowedFieldFSLayers) == 0 { - unexpected = append(unexpected, "fsLayers") - } - if detectedFields.History != nil && (allowed&allowedFieldHistory) == 0 { - unexpected = append(unexpected, "history") - } - if detectedFields.Layers != nil && (allowed&allowedFieldLayers) == 0 { - unexpected = append(unexpected, "layers") - } - if detectedFields.Manifests != nil && (allowed&allowedFieldManifests) == 0 { - unexpected = append(unexpected, "manifests") - } - if len(unexpected) != 0 { - return fmt.Errorf(`rejecting ambiguous manifest, unexpected fields %#v in supposedly %s`, - unexpected, expectedMIMEType) - } - return nil -} - // layerInfosToStrings converts a list of layer infos, presumably obtained from a Manifest.LayerInfos() // method call, into a format suitable for inclusion in a types.ImageInspectInfo structure. func layerInfosToStrings(infos []LayerInfo) []string { diff --git a/manifest/common_test.go b/manifest/common_test.go index 15b92f88e0..37cb8a63da 100644 --- a/manifest/common_test.go +++ b/manifest/common_test.go @@ -15,56 +15,6 @@ import ( "github.com/stretchr/testify/require" ) -func TestValidateUnambiguousManifestFormat(t *testing.T) { - const allAllowedFields = allowedFieldFirstUnusedBit - 1 - const mt = "text/plain" // Just some MIME type that shows up in error messages - - type test struct { - manifest string - allowed allowedManifestFields - } - - // Smoke tests: Success - for _, c := range []test{ - {"{}", allAllowedFields}, - {"{}", 0}, - } { - err := validateUnambiguousManifestFormat([]byte(c.manifest), mt, c.allowed) - assert.NoError(t, err, c) - } - // Smoke tests: Failure - for _, c := range []test{ - {"{}", allowedFieldFirstUnusedBit}, // Invalid "allowed" - {"@", allAllowedFields}, // Invalid JSON - } { - err := validateUnambiguousManifestFormat([]byte(c.manifest), mt, c.allowed) - assert.Error(t, err, c) - } - - fields := map[allowedManifestFields]string{ - allowedFieldConfig: "config", - allowedFieldFSLayers: "fsLayers", - allowedFieldHistory: "history", - allowedFieldLayers: "layers", - allowedFieldManifests: "manifests", - } - // Ensure this test covers all defined allowedManifestFields values - allFields := allowedManifestFields(0) - for k := range fields { - allFields |= k - } - assert.Equal(t, allAllowedFields, allFields) - - // Every single field is allowed by its bit, and rejected by any other bit - for bit, fieldName := range fields { - json := []byte(fmt.Sprintf(`{"%s":[]}`, fieldName)) - err := validateUnambiguousManifestFormat(json, mt, bit) - assert.NoError(t, err, fieldName) - err = validateUnambiguousManifestFormat(json, mt, allAllowedFields^bit) - assert.Error(t, err, fieldName) - } -} - // Test that parser() rejects all of the provided manifest fixtures. // Intended to help test manifest parsers' detection of schema mismatches. func testManifestFixturesAreRejected(t *testing.T, parser func([]byte) error, fixtures []string) { diff --git a/manifest/docker_schema1.go b/manifest/docker_schema1.go index 9df9bc6673..7b9c4b58fb 100644 --- a/manifest/docker_schema1.go +++ b/manifest/docker_schema1.go @@ -4,14 +4,17 @@ import ( "encoding/json" "errors" "fmt" - "regexp" "strings" "time" "github.com/containers/image/v5/docker/reference" + "github.com/containers/image/v5/internal/manifest" + "github.com/containers/image/v5/internal/set" "github.com/containers/image/v5/types" + "github.com/containers/storage/pkg/regexp" "github.com/docker/docker/api/types/versions" "github.com/opencontainers/go-digest" + "golang.org/x/exp/slices" ) // Schema1FSLayers is an entry of the "fsLayers" array in docker/distribution schema 1. @@ -53,16 +56,16 @@ type Schema1V1Compatibility struct { // Schema1FromManifest creates a Schema1 manifest instance from a manifest blob. // (NOTE: The instance is not necessary a literal representation of the original blob, // layers with duplicate IDs are eliminated.) -func Schema1FromManifest(manifest []byte) (*Schema1, error) { +func Schema1FromManifest(manifestBlob []byte) (*Schema1, error) { s1 := Schema1{} - if err := json.Unmarshal(manifest, &s1); err != nil { + if err := json.Unmarshal(manifestBlob, &s1); err != nil { return nil, err } if s1.SchemaVersion != 1 { return nil, fmt.Errorf("unsupported schema version %d", s1.SchemaVersion) } - if err := validateUnambiguousManifestFormat(manifest, DockerV2Schema1SignedMediaType, - allowedFieldFSLayers|allowedFieldHistory); err != nil { + if err := manifest.ValidateUnambiguousManifestFormat(manifestBlob, DockerV2Schema1SignedMediaType, + manifest.AllowedFieldFSLayers|manifest.AllowedFieldHistory); err != nil { return nil, err } if err := s1.initialize(); err != nil { @@ -183,22 +186,22 @@ func (m *Schema1) fixManifestLayers() error { return errors.New("Invalid parent ID in the base layer of the image") } // check general duplicates to error instead of a deadlock - idmap := make(map[string]struct{}) + idmap := set.New[string]() var lastID string for _, img := range m.ExtractedV1Compatibility { // skip IDs that appear after each other, we handle those later - if _, exists := idmap[img.ID]; img.ID != lastID && exists { + if img.ID != lastID && idmap.Contains(img.ID) { return fmt.Errorf("ID %+v appears multiple times in manifest", img.ID) } lastID = img.ID - idmap[lastID] = struct{}{} + idmap.Add(lastID) } // backwards loop so that we keep the remaining indexes after removing items for i := len(m.ExtractedV1Compatibility) - 2; i >= 0; i-- { if m.ExtractedV1Compatibility[i].ID == m.ExtractedV1Compatibility[i+1].ID { // repeated ID. remove and continue - m.FSLayers = append(m.FSLayers[:i], m.FSLayers[i+1:]...) - m.History = append(m.History[:i], m.History[i+1:]...) - m.ExtractedV1Compatibility = append(m.ExtractedV1Compatibility[:i], m.ExtractedV1Compatibility[i+1:]...) + m.FSLayers = slices.Delete(m.FSLayers, i, i+1) + m.History = slices.Delete(m.History, i, i+1) + m.ExtractedV1Compatibility = slices.Delete(m.ExtractedV1Compatibility, i, i+1) } else if m.ExtractedV1Compatibility[i].Parent != m.ExtractedV1Compatibility[i+1].ID { return fmt.Errorf("Invalid parent ID. Expected %v, got %v", m.ExtractedV1Compatibility[i+1].ID, m.ExtractedV1Compatibility[i].Parent) } @@ -206,7 +209,7 @@ func (m *Schema1) fixManifestLayers() error { return nil } -var validHex = regexp.MustCompile(`^([a-f0-9]{64})$`) +var validHex = regexp.Delayed(`^([a-f0-9]{64})$`) func validateV1ID(id string) error { if ok := validHex.MatchString(id); !ok { @@ -227,6 +230,7 @@ func (m *Schema1) Inspect(_ func(types.BlobInfo) ([]byte, error)) (*types.ImageI Created: &s1.Created, DockerVersion: s1.DockerVersion, Architecture: s1.Architecture, + Variant: s1.Variant, Os: s1.OS, Layers: layerInfosToStrings(layerInfos), LayersData: imgInspectLayersFromLayerInfos(layerInfos), diff --git a/manifest/docker_schema2.go b/manifest/docker_schema2.go index d9eca043be..3c9745dde5 100644 --- a/manifest/docker_schema2.go +++ b/manifest/docker_schema2.go @@ -5,6 +5,7 @@ import ( "fmt" "time" + "github.com/containers/image/v5/internal/manifest" compressiontypes "github.com/containers/image/v5/pkg/compression/types" "github.com/containers/image/v5/pkg/strslice" "github.com/containers/image/v5/types" @@ -12,12 +13,7 @@ import ( ) // Schema2Descriptor is a “descriptor” in docker/distribution schema 2. -type Schema2Descriptor struct { - MediaType string `json:"mediaType"` - Size int64 `json:"size"` - Digest digest.Digest `json:"digest"` - URLs []string `json:"urls,omitempty"` -} +type Schema2Descriptor = manifest.Schema2Descriptor // BlobInfoFromSchema2Descriptor returns a types.BlobInfo based on the input schema 2 descriptor. func BlobInfoFromSchema2Descriptor(desc Schema2Descriptor) types.BlobInfo { @@ -159,13 +155,13 @@ type Schema2Image struct { } // Schema2FromManifest creates a Schema2 manifest instance from a manifest blob. -func Schema2FromManifest(manifest []byte) (*Schema2, error) { +func Schema2FromManifest(manifestBlob []byte) (*Schema2, error) { s2 := Schema2{} - if err := json.Unmarshal(manifest, &s2); err != nil { + if err := json.Unmarshal(manifestBlob, &s2); err != nil { return nil, err } - if err := validateUnambiguousManifestFormat(manifest, DockerV2Schema2MediaType, - allowedFieldConfig|allowedFieldLayers); err != nil { + if err := manifest.ValidateUnambiguousManifestFormat(manifestBlob, DockerV2Schema2MediaType, + manifest.AllowedFieldConfig|manifest.AllowedFieldLayers); err != nil { return nil, err } // Check manifest's and layers' media types. diff --git a/manifest/docker_schema2_list.go b/manifest/docker_schema2_list.go index 7e4cc51835..c958a3fa3a 100644 --- a/manifest/docker_schema2_list.go +++ b/manifest/docker_schema2_list.go @@ -1,220 +1,32 @@ package manifest import ( - "encoding/json" - "fmt" - - platform "github.com/containers/image/v5/internal/pkg/platform" - "github.com/containers/image/v5/types" - "github.com/opencontainers/go-digest" - imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/containers/image/v5/internal/manifest" ) // Schema2PlatformSpec describes the platform which a particular manifest is // specialized for. -type Schema2PlatformSpec struct { - Architecture string `json:"architecture"` - OS string `json:"os"` - OSVersion string `json:"os.version,omitempty"` - OSFeatures []string `json:"os.features,omitempty"` - Variant string `json:"variant,omitempty"` - Features []string `json:"features,omitempty"` // removed in OCI -} +type Schema2PlatformSpec = manifest.Schema2PlatformSpec // Schema2ManifestDescriptor references a platform-specific manifest. -type Schema2ManifestDescriptor struct { - Schema2Descriptor - Platform Schema2PlatformSpec `json:"platform"` -} +type Schema2ManifestDescriptor = manifest.Schema2ManifestDescriptor // Schema2List is a list of platform-specific manifests. -type Schema2List struct { - SchemaVersion int `json:"schemaVersion"` - MediaType string `json:"mediaType"` - Manifests []Schema2ManifestDescriptor `json:"manifests"` -} - -// MIMEType returns the MIME type of this particular manifest list. -func (list *Schema2List) MIMEType() string { - return list.MediaType -} - -// Instances returns a slice of digests of the manifests that this list knows of. -func (list *Schema2List) Instances() []digest.Digest { - results := make([]digest.Digest, len(list.Manifests)) - for i, m := range list.Manifests { - results[i] = m.Digest - } - return results -} - -// Instance returns the ListUpdate of a particular instance in the list. -func (list *Schema2List) Instance(instanceDigest digest.Digest) (ListUpdate, error) { - for _, manifest := range list.Manifests { - if manifest.Digest == instanceDigest { - return ListUpdate{ - Digest: manifest.Digest, - Size: manifest.Size, - MediaType: manifest.MediaType, - }, nil - } - } - return ListUpdate{}, fmt.Errorf("unable to find instance %s passed to Schema2List.Instances", instanceDigest) -} - -// UpdateInstances updates the sizes, digests, and media types of the manifests -// which the list catalogs. -func (list *Schema2List) UpdateInstances(updates []ListUpdate) error { - if len(updates) != len(list.Manifests) { - return fmt.Errorf("incorrect number of update entries passed to Schema2List.UpdateInstances: expected %d, got %d", len(list.Manifests), len(updates)) - } - for i := range updates { - if err := updates[i].Digest.Validate(); err != nil { - return fmt.Errorf("update %d of %d passed to Schema2List.UpdateInstances contained an invalid digest: %w", i+1, len(updates), err) - } - list.Manifests[i].Digest = updates[i].Digest - if updates[i].Size < 0 { - return fmt.Errorf("update %d of %d passed to Schema2List.UpdateInstances had an invalid size (%d)", i+1, len(updates), updates[i].Size) - } - list.Manifests[i].Size = updates[i].Size - if updates[i].MediaType == "" { - return fmt.Errorf("update %d of %d passed to Schema2List.UpdateInstances had no media type (was %q)", i+1, len(updates), list.Manifests[i].MediaType) - } - list.Manifests[i].MediaType = updates[i].MediaType - } - return nil -} - -// ChooseInstance parses blob as a schema2 manifest list, and returns the digest -// of the image which is appropriate for the current environment. -func (list *Schema2List) ChooseInstance(ctx *types.SystemContext) (digest.Digest, error) { - wantedPlatforms, err := platform.WantedPlatforms(ctx) - if err != nil { - return "", fmt.Errorf("getting platform information %#v: %w", ctx, err) - } - for _, wantedPlatform := range wantedPlatforms { - for _, d := range list.Manifests { - imagePlatform := imgspecv1.Platform{ - Architecture: d.Platform.Architecture, - OS: d.Platform.OS, - OSVersion: d.Platform.OSVersion, - OSFeatures: dupStringSlice(d.Platform.OSFeatures), - Variant: d.Platform.Variant, - } - if platform.MatchesPlatform(imagePlatform, wantedPlatform) { - return d.Digest, nil - } - } - } - return "", fmt.Errorf("no image found in manifest list for architecture %s, variant %q, OS %s", wantedPlatforms[0].Architecture, wantedPlatforms[0].Variant, wantedPlatforms[0].OS) -} - -// Serialize returns the list in a blob format. -// NOTE: Serialize() does not in general reproduce the original blob if this object was loaded from one, even if no modifications were made! -func (list *Schema2List) Serialize() ([]byte, error) { - buf, err := json.Marshal(list) - if err != nil { - return nil, fmt.Errorf("marshaling Schema2List %#v: %w", list, err) - } - return buf, nil -} +type Schema2List = manifest.Schema2ListPublic // Schema2ListFromComponents creates a Schema2 manifest list instance from the // supplied data. func Schema2ListFromComponents(components []Schema2ManifestDescriptor) *Schema2List { - list := Schema2List{ - SchemaVersion: 2, - MediaType: DockerV2ListMediaType, - Manifests: make([]Schema2ManifestDescriptor, len(components)), - } - for i, component := range components { - m := Schema2ManifestDescriptor{ - Schema2Descriptor{ - MediaType: component.MediaType, - Size: component.Size, - Digest: component.Digest, - URLs: dupStringSlice(component.URLs), - }, - Schema2PlatformSpec{ - Architecture: component.Platform.Architecture, - OS: component.Platform.OS, - OSVersion: component.Platform.OSVersion, - OSFeatures: dupStringSlice(component.Platform.OSFeatures), - Variant: component.Platform.Variant, - Features: dupStringSlice(component.Platform.Features), - }, - } - list.Manifests[i] = m - } - return &list + return manifest.Schema2ListPublicFromComponents(components) } // Schema2ListClone creates a deep copy of the passed-in list. func Schema2ListClone(list *Schema2List) *Schema2List { - return Schema2ListFromComponents(list.Manifests) -} - -// ToOCI1Index returns the list encoded as an OCI1 index. -func (list *Schema2List) ToOCI1Index() (*OCI1Index, error) { - components := make([]imgspecv1.Descriptor, 0, len(list.Manifests)) - for _, manifest := range list.Manifests { - converted := imgspecv1.Descriptor{ - MediaType: manifest.MediaType, - Size: manifest.Size, - Digest: manifest.Digest, - URLs: dupStringSlice(manifest.URLs), - Platform: &imgspecv1.Platform{ - OS: manifest.Platform.OS, - Architecture: manifest.Platform.Architecture, - OSFeatures: dupStringSlice(manifest.Platform.OSFeatures), - OSVersion: manifest.Platform.OSVersion, - Variant: manifest.Platform.Variant, - }, - } - components = append(components, converted) - } - oci := OCI1IndexFromComponents(components, nil) - return oci, nil -} - -// ToSchema2List returns the list encoded as a Schema2 list. -func (list *Schema2List) ToSchema2List() (*Schema2List, error) { - return Schema2ListClone(list), nil + return manifest.Schema2ListPublicClone(list) } // Schema2ListFromManifest creates a Schema2 manifest list instance from marshalled // JSON, presumably generated by encoding a Schema2 manifest list. -func Schema2ListFromManifest(manifest []byte) (*Schema2List, error) { - list := Schema2List{ - Manifests: []Schema2ManifestDescriptor{}, - } - if err := json.Unmarshal(manifest, &list); err != nil { - return nil, fmt.Errorf("unmarshaling Schema2List %q: %w", string(manifest), err) - } - if err := validateUnambiguousManifestFormat(manifest, DockerV2ListMediaType, - allowedFieldManifests); err != nil { - return nil, err - } - return &list, nil -} - -// Clone returns a deep copy of this list and its contents. -func (list *Schema2List) Clone() List { - return Schema2ListClone(list) -} - -// ConvertToMIMEType converts the passed-in manifest list to a manifest -// list of the specified type. -func (list *Schema2List) ConvertToMIMEType(manifestMIMEType string) (List, error) { - switch normalized := NormalizedMIMEType(manifestMIMEType); normalized { - case DockerV2ListMediaType: - return list.Clone(), nil - case imgspecv1.MediaTypeImageIndex: - return list.ToOCI1Index() - case DockerV2Schema1MediaType, DockerV2Schema1SignedMediaType, imgspecv1.MediaTypeImageManifest, DockerV2Schema2MediaType: - return nil, fmt.Errorf("Can not convert manifest list to MIME type %q, which is not a list type", manifestMIMEType) - default: - // Note that this may not be reachable, NormalizedMIMEType has a default for unknown values. - return nil, fmt.Errorf("Unimplemented manifest list MIME type %s", manifestMIMEType) - } +func Schema2ListFromManifest(manifestBlob []byte) (*Schema2List, error) { + return manifest.Schema2ListPublicFromManifest(manifestBlob) } diff --git a/manifest/fixtures/non-json.manifest.json b/manifest/fixtures/non-json.manifest.json deleted file mode 100644 index f892721275..0000000000 Binary files a/manifest/fixtures/non-json.manifest.json and /dev/null differ diff --git a/manifest/fixtures/non-json.manifest.json b/manifest/fixtures/non-json.manifest.json new file mode 120000 index 0000000000..367b7de9c9 --- /dev/null +++ b/manifest/fixtures/non-json.manifest.json @@ -0,0 +1 @@ +../../internal/manifest/testdata/non-json.manifest.json \ No newline at end of file diff --git a/manifest/fixtures/ociv1.artifact.json b/manifest/fixtures/ociv1.artifact.json deleted file mode 100644 index a53807924e..0000000000 --- a/manifest/fixtures/ociv1.artifact.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "schemaVersion": 2, - "mediaType": "application/vnd.oci.image.manifest.v1+json", - "config": { - "mediaType": "application/vnd.oci.custom.artifact.config.v1+json", - "digest": "", - "size": 0 - }, - "layers": null -} diff --git a/manifest/fixtures/ociv1.artifact.json b/manifest/fixtures/ociv1.artifact.json new file mode 120000 index 0000000000..fcec28a2fe --- /dev/null +++ b/manifest/fixtures/ociv1.artifact.json @@ -0,0 +1 @@ +../../internal/manifest/testdata/ociv1.artifact.json \ No newline at end of file diff --git a/manifest/fixtures/ociv1.image.index.json b/manifest/fixtures/ociv1.image.index.json deleted file mode 100644 index a85b4d889c..0000000000 --- a/manifest/fixtures/ociv1.image.index.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "schemaVersion": 2, - "mediaType": "application/vnd.oci.image.index.v1+json", - "manifests": [ - { - "mediaType": "application/vnd.oci.image.manifest.v1+json", - "size": 7143, - "digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f", - "platform": { - "architecture": "ppc64le", - "os": "linux" - } - }, - { - "mediaType": "application/vnd.oci.image.manifest.v1+json", - "size": 7682, - "digest": "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270", - "platform": { - "architecture": "amd64", - "os": "linux", - "os.features": [ - "sse4" - ] - } - } - ], - "annotations": { - "com.example.key1": "value1", - "com.example.key2": "value2" - } -} diff --git a/manifest/fixtures/ociv1.image.index.json b/manifest/fixtures/ociv1.image.index.json new file mode 120000 index 0000000000..d5373fcfee --- /dev/null +++ b/manifest/fixtures/ociv1.image.index.json @@ -0,0 +1 @@ +../../internal/manifest/testdata/ociv1.image.index.json \ No newline at end of file diff --git a/manifest/fixtures/ociv1.manifest.json b/manifest/fixtures/ociv1.manifest.json deleted file mode 100644 index 7e2e2e8274..0000000000 --- a/manifest/fixtures/ociv1.manifest.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "schemaVersion": 2, - "mediaType": "application/vnd.oci.image.manifest.v1+json", - "config": { - "mediaType": "application/vnd.oci.image.config.v1+json", - "size": 7023, - "digest": "sha256:b5b2b2c507a0944348e0303114d8d93aaaa081732b86451d9bce1f432a537bc7" - }, - "layers": [ - { - "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", - "size": 32654, - "digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f" - }, - { - "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", - "size": 16724, - "digest": "sha256:3c3a4604a545cdc127456d94e421cd355bca5b528f4a9c1905b15da2eb4a4c6b" - }, - { - "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", - "size": 73109, - "digest": "sha256:ec4b8955958665577945c89419d1af06b5f7636b4ac3da7f12184802ad867736" - } - ], - "annotations": { - "com.example.key1": "value1", - "com.example.key2": "value2" - } -} diff --git a/manifest/fixtures/ociv1.manifest.json b/manifest/fixtures/ociv1.manifest.json new file mode 120000 index 0000000000..4927dc985b --- /dev/null +++ b/manifest/fixtures/ociv1.manifest.json @@ -0,0 +1 @@ +../../internal/manifest/testdata/ociv1.manifest.json \ No newline at end of file diff --git a/manifest/fixtures/ociv1nomime.artifact.json b/manifest/fixtures/ociv1nomime.artifact.json deleted file mode 100644 index 5091d1f61e..0000000000 --- a/manifest/fixtures/ociv1nomime.artifact.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "schemaVersion": 2, - "config": { - "mediaType": "application/vnd.oci.custom.artifact.config.v1+json", - "digest": "", - "size": 0 - }, - "layers": null -} diff --git a/manifest/fixtures/ociv1nomime.artifact.json b/manifest/fixtures/ociv1nomime.artifact.json new file mode 120000 index 0000000000..4b1ebfb0f3 --- /dev/null +++ b/manifest/fixtures/ociv1nomime.artifact.json @@ -0,0 +1 @@ +../../internal/manifest/testdata/ociv1nomime.artifact.json \ No newline at end of file diff --git a/manifest/fixtures/ociv1nomime.image.index.json b/manifest/fixtures/ociv1nomime.image.index.json deleted file mode 100644 index 066f058db1..0000000000 --- a/manifest/fixtures/ociv1nomime.image.index.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "schemaVersion": 2, - "manifests": [ - { - "mediaType": "application/vnd.oci.image.manifest.v1+json", - "size": 7143, - "digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f", - "platform": { - "architecture": "ppc64le", - "os": "linux" - } - }, - { - "mediaType": "application/vnd.oci.image.manifest.v1+json", - "size": 7682, - "digest": "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270", - "platform": { - "architecture": "amd64", - "os": "linux", - "os.features": [ - "sse4" - ] - } - } - ], - "annotations": { - "com.example.key1": "value1", - "com.example.key2": "value2" - } -} diff --git a/manifest/fixtures/ociv1nomime.image.index.json b/manifest/fixtures/ociv1nomime.image.index.json new file mode 120000 index 0000000000..29b3d9be46 --- /dev/null +++ b/manifest/fixtures/ociv1nomime.image.index.json @@ -0,0 +1 @@ +../../internal/manifest/testdata/ociv1nomime.image.index.json \ No newline at end of file diff --git a/manifest/fixtures/ociv1nomime.manifest.json b/manifest/fixtures/ociv1nomime.manifest.json deleted file mode 100644 index 1e1047ca7f..0000000000 --- a/manifest/fixtures/ociv1nomime.manifest.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "schemaVersion": 2, - "config": { - "mediaType": "application/vnd.oci.image.config.v1+json", - "size": 7023, - "digest": "sha256:b5b2b2c507a0944348e0303114d8d93aaaa081732b86451d9bce1f432a537bc7" - }, - "layers": [ - { - "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", - "size": 32654, - "digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f" - }, - { - "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", - "size": 16724, - "digest": "sha256:3c3a4604a545cdc127456d94e421cd355bca5b528f4a9c1905b15da2eb4a4c6b" - }, - { - "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", - "size": 73109, - "digest": "sha256:ec4b8955958665577945c89419d1af06b5f7636b4ac3da7f12184802ad867736" - } - ], - "annotations": { - "com.example.key1": "value1", - "com.example.key2": "value2" - } -} diff --git a/manifest/fixtures/ociv1nomime.manifest.json b/manifest/fixtures/ociv1nomime.manifest.json new file mode 120000 index 0000000000..037a21ff9b --- /dev/null +++ b/manifest/fixtures/ociv1nomime.manifest.json @@ -0,0 +1 @@ +../../internal/manifest/testdata/ociv1nomime.manifest.json \ No newline at end of file diff --git a/manifest/fixtures/schema2-to-schema1-by-docker.json b/manifest/fixtures/schema2-to-schema1-by-docker.json index 2073e28868..322c5b7415 120000 --- a/manifest/fixtures/schema2-to-schema1-by-docker.json +++ b/manifest/fixtures/schema2-to-schema1-by-docker.json @@ -1 +1 @@ -../../internal/image/fixtures/schema2-to-schema1-by-docker.json \ No newline at end of file +../../internal/manifest/testdata/schema2-to-schema1-by-docker.json \ No newline at end of file diff --git a/manifest/fixtures/unknown-version.manifest.json b/manifest/fixtures/unknown-version.manifest.json deleted file mode 100644 index b0f34b631c..0000000000 --- a/manifest/fixtures/unknown-version.manifest.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "schemaVersion": 99999, - "name": "mitr/noversion-nonsense", - "tag": "latest" -} diff --git a/manifest/fixtures/unknown-version.manifest.json b/manifest/fixtures/unknown-version.manifest.json new file mode 120000 index 0000000000..c9136e913c --- /dev/null +++ b/manifest/fixtures/unknown-version.manifest.json @@ -0,0 +1 @@ +../../internal/manifest/testdata/unknown-version.manifest.json \ No newline at end of file diff --git a/manifest/fixtures/v2list.manifest.json b/manifest/fixtures/v2list.manifest.json deleted file mode 100644 index 1bf9896e04..0000000000 --- a/manifest/fixtures/v2list.manifest.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "schemaVersion": 2, - "mediaType": "application/vnd.docker.distribution.manifest.list.v2+json", - "manifests": [ - { - "mediaType": "application/vnd.docker.distribution.manifest.v1+json", - "size": 2094, - "digest": "sha256:7820f9a86d4ad15a2c4f0c0e5479298df2aa7c2f6871288e2ef8546f3e7b6783", - "platform": { - "architecture": "ppc64le", - "os": "linux" - } - }, - { - "mediaType": "application/vnd.docker.distribution.manifest.v1+json", - "size": 1922, - "digest": "sha256:ae1b0e06e8ade3a11267564a26e750585ba2259c0ecab59ab165ad1af41d1bdd", - "platform": { - "architecture": "amd64", - "os": "linux", - "features": [ - "sse" - ] - } - }, - { - "mediaType": "application/vnd.docker.distribution.manifest.v1+json", - "size": 2084, - "digest": "sha256:e4c0df75810b953d6717b8f8f28298d73870e8aa2a0d5e77b8391f16fdfbbbe2", - "platform": { - "architecture": "s390x", - "os": "linux" - } - }, - { - "mediaType": "application/vnd.docker.distribution.manifest.v1+json", - "size": 2084, - "digest": "sha256:07ebe243465ef4a667b78154ae6c3ea46fdb1582936aac3ac899ea311a701b40", - "platform": { - "architecture": "arm", - "os": "linux", - "variant": "armv7" - } - }, - { - "mediaType": "application/vnd.docker.distribution.manifest.v1+json", - "size": 2090, - "digest": "sha256:fb2fc0707b86dafa9959fe3d29e66af8787aee4d9a23581714be65db4265ad8a", - "platform": { - "architecture": "arm64", - "os": "linux", - "variant": "armv8" - } - } - ] -} diff --git a/manifest/fixtures/v2list.manifest.json b/manifest/fixtures/v2list.manifest.json new file mode 120000 index 0000000000..8fb6441786 --- /dev/null +++ b/manifest/fixtures/v2list.manifest.json @@ -0,0 +1 @@ +../../internal/manifest/testdata/v2list.manifest.json \ No newline at end of file diff --git a/manifest/fixtures/v2s1-invalid-signatures.manifest.json b/manifest/fixtures/v2s1-invalid-signatures.manifest.json deleted file mode 100644 index 96def40232..0000000000 --- a/manifest/fixtures/v2s1-invalid-signatures.manifest.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "schemaVersion": 1, - "name": "mitr/busybox", - "tag": "latest", - "architecture": "amd64", - "fsLayers": [ - ], - "history": [ - ], - "signatures": 1 -} diff --git a/manifest/fixtures/v2s1-invalid-signatures.manifest.json b/manifest/fixtures/v2s1-invalid-signatures.manifest.json new file mode 120000 index 0000000000..832703ed56 --- /dev/null +++ b/manifest/fixtures/v2s1-invalid-signatures.manifest.json @@ -0,0 +1 @@ +../../internal/manifest/testdata/v2s1-invalid-signatures.manifest.json \ No newline at end of file diff --git a/manifest/fixtures/v2s1-unsigned.manifest.json b/manifest/fixtures/v2s1-unsigned.manifest.json deleted file mode 100644 index 16764b409e..0000000000 --- a/manifest/fixtures/v2s1-unsigned.manifest.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "schemaVersion": 1, - "name": "mitr/busybox", - "tag": "latest", - "architecture": "amd64", - "fsLayers": [ - { - "blobSum": "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef" - }, - { - "blobSum": "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef" - }, - { - "blobSum": "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef" - } - ], - "history": [ - { - "v1Compatibility": "{\"id\":\"f1b5eb0a1215f663765d509b6cdf3841bc2bcff0922346abb943d1342d469a97\",\"parent\":\"594075be8d003f784074cc639d970d1fa091a8197850baaae5052c01564ac535\",\"created\":\"2016-03-03T11:29:44.222098366Z\",\"container\":\"c0924f5b281a1992127d0afc065e59548ded8880b08aea4debd56d4497acb17a\",\"container_config\":{\"Hostname\":\"56f0fe1dfc95\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":null,\"PublishService\":\"\",\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) LABEL Checksum=4fef81d30f31f9213c642881357e6662846a0f884c2366c13ebad807b4031368 ./tests/test-images/Dockerfile.2\"],\"Image\":\"594075be8d003f784074cc639d970d1fa091a8197850baaae5052c01564ac535\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":{\"Checksum\":\"4fef81d30f31f9213c642881357e6662846a0f884c2366c13ebad807b4031368 ./tests/test-images/Dockerfile.2\",\"Name\":\"atomic-test-2\"}},\"docker_version\":\"1.8.2-fc22\",\"author\":\"\\\"William Temple \\u003cwtemple at redhat dot com\\u003e\\\"\",\"config\":{\"Hostname\":\"56f0fe1dfc95\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":null,\"PublishService\":\"\",\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":null,\"Image\":\"594075be8d003f784074cc639d970d1fa091a8197850baaae5052c01564ac535\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":{\"Checksum\":\"4fef81d30f31f9213c642881357e6662846a0f884c2366c13ebad807b4031368 ./tests/test-images/Dockerfile.2\",\"Name\":\"atomic-test-2\"}},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":0}\n" - }, - { - "v1Compatibility": "{\"id\":\"594075be8d003f784074cc639d970d1fa091a8197850baaae5052c01564ac535\",\"parent\":\"03dfa1cd1abe452bc2b69b8eb2362fa6beebc20893e65437906318954f6276d4\",\"created\":\"2016-03-03T11:29:38.563048924Z\",\"container\":\"fd4cf54dcd239fbae9bdade9db48e41880b436d27cb5313f60952a46ab04deff\",\"container_config\":{\"Hostname\":\"56f0fe1dfc95\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":null,\"PublishService\":\"\",\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) LABEL Name=atomic-test-2\"],\"Image\":\"03dfa1cd1abe452bc2b69b8eb2362fa6beebc20893e65437906318954f6276d4\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":{\"Name\":\"atomic-test-2\"}},\"docker_version\":\"1.8.2-fc22\",\"author\":\"\\\"William Temple \\u003cwtemple at redhat dot com\\u003e\\\"\",\"config\":{\"Hostname\":\"56f0fe1dfc95\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":null,\"PublishService\":\"\",\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":null,\"Image\":\"03dfa1cd1abe452bc2b69b8eb2362fa6beebc20893e65437906318954f6276d4\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":{\"Name\":\"atomic-test-2\"}},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":0}\n" - }, - { - "v1Compatibility": "{\"id\":\"03dfa1cd1abe452bc2b69b8eb2362fa6beebc20893e65437906318954f6276d4\",\"created\":\"2016-03-03T11:29:32.948089874Z\",\"container\":\"56f0fe1dfc95755dd6cda10f7215c9937a8d9c6348d079c581a261fd4c2f3a5f\",\"container_config\":{\"Hostname\":\"56f0fe1dfc95\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":null,\"PublishService\":\"\",\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) MAINTAINER \\\"William Temple \\u003cwtemple at redhat dot com\\u003e\\\"\"],\"Image\":\"\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":null},\"docker_version\":\"1.8.2-fc22\",\"author\":\"\\\"William Temple \\u003cwtemple at redhat dot com\\u003e\\\"\",\"config\":{\"Hostname\":\"56f0fe1dfc95\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":null,\"PublishService\":\"\",\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":null,\"Image\":\"\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":null},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":0}\n" - } - ] -} \ No newline at end of file diff --git a/manifest/fixtures/v2s1-unsigned.manifest.json b/manifest/fixtures/v2s1-unsigned.manifest.json new file mode 120000 index 0000000000..d6a55a7475 --- /dev/null +++ b/manifest/fixtures/v2s1-unsigned.manifest.json @@ -0,0 +1 @@ +../../internal/manifest/testdata/v2s1-unsigned.manifest.json \ No newline at end of file diff --git a/manifest/fixtures/v2s1.manifest.json b/manifest/fixtures/v2s1.manifest.json deleted file mode 100644 index f7bcd07401..0000000000 --- a/manifest/fixtures/v2s1.manifest.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "schemaVersion": 1, - "name": "mitr/busybox", - "tag": "latest", - "architecture": "amd64", - "fsLayers": [ - { - "blobSum": "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef" - }, - { - "blobSum": "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef" - }, - { - "blobSum": "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef" - } - ], - "history": [ - { - "v1Compatibility": "{\"id\":\"f1b5eb0a1215f663765d509b6cdf3841bc2bcff0922346abb943d1342d469a97\",\"parent\":\"594075be8d003f784074cc639d970d1fa091a8197850baaae5052c01564ac535\",\"created\":\"2016-03-03T11:29:44.222098366Z\",\"container\":\"c0924f5b281a1992127d0afc065e59548ded8880b08aea4debd56d4497acb17a\",\"container_config\":{\"Hostname\":\"56f0fe1dfc95\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":null,\"PublishService\":\"\",\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) LABEL Checksum=4fef81d30f31f9213c642881357e6662846a0f884c2366c13ebad807b4031368 ./tests/test-images/Dockerfile.2\"],\"Image\":\"594075be8d003f784074cc639d970d1fa091a8197850baaae5052c01564ac535\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":{\"Checksum\":\"4fef81d30f31f9213c642881357e6662846a0f884c2366c13ebad807b4031368 ./tests/test-images/Dockerfile.2\",\"Name\":\"atomic-test-2\"}},\"docker_version\":\"1.8.2-fc22\",\"author\":\"\\\"William Temple \\u003cwtemple at redhat dot com\\u003e\\\"\",\"config\":{\"Hostname\":\"56f0fe1dfc95\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":null,\"PublishService\":\"\",\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":null,\"Image\":\"594075be8d003f784074cc639d970d1fa091a8197850baaae5052c01564ac535\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":{\"Checksum\":\"4fef81d30f31f9213c642881357e6662846a0f884c2366c13ebad807b4031368 ./tests/test-images/Dockerfile.2\",\"Name\":\"atomic-test-2\"}},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":0}\n" - }, - { - "v1Compatibility": "{\"id\":\"594075be8d003f784074cc639d970d1fa091a8197850baaae5052c01564ac535\",\"parent\":\"03dfa1cd1abe452bc2b69b8eb2362fa6beebc20893e65437906318954f6276d4\",\"created\":\"2016-03-03T11:29:38.563048924Z\",\"container\":\"fd4cf54dcd239fbae9bdade9db48e41880b436d27cb5313f60952a46ab04deff\",\"container_config\":{\"Hostname\":\"56f0fe1dfc95\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":null,\"PublishService\":\"\",\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) LABEL Name=atomic-test-2\"],\"Image\":\"03dfa1cd1abe452bc2b69b8eb2362fa6beebc20893e65437906318954f6276d4\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":{\"Name\":\"atomic-test-2\"}},\"docker_version\":\"1.8.2-fc22\",\"author\":\"\\\"William Temple \\u003cwtemple at redhat dot com\\u003e\\\"\",\"config\":{\"Hostname\":\"56f0fe1dfc95\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":null,\"PublishService\":\"\",\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":null,\"Image\":\"03dfa1cd1abe452bc2b69b8eb2362fa6beebc20893e65437906318954f6276d4\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":{\"Name\":\"atomic-test-2\"}},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":0}\n" - }, - { - "v1Compatibility": "{\"id\":\"03dfa1cd1abe452bc2b69b8eb2362fa6beebc20893e65437906318954f6276d4\",\"created\":\"2016-03-03T11:29:32.948089874Z\",\"container\":\"56f0fe1dfc95755dd6cda10f7215c9937a8d9c6348d079c581a261fd4c2f3a5f\",\"container_config\":{\"Hostname\":\"56f0fe1dfc95\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":null,\"PublishService\":\"\",\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) MAINTAINER \\\"William Temple \\u003cwtemple at redhat dot com\\u003e\\\"\"],\"Image\":\"\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":null},\"docker_version\":\"1.8.2-fc22\",\"author\":\"\\\"William Temple \\u003cwtemple at redhat dot com\\u003e\\\"\",\"config\":{\"Hostname\":\"56f0fe1dfc95\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":null,\"PublishService\":\"\",\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":null,\"Image\":\"\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":null},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":0}\n" - } - ], - "signatures": [ - { - "header": { - "jwk": { - "crv": "P-256", - "kid": "OZ45:U3IG:TDOI:PMBD:NGP2:LDIW:II2U:PSBI:MMCZ:YZUP:TUUO:XPZT", - "kty": "EC", - "x": "ReC5c0J9tgXSdUL4_xzEt5RsD8kFt2wWSgJcpAcOQx8", - "y": "3sBGEqQ3ZMeqPKwQBAadN2toOUEASha18xa0WwsDF-M" - }, - "alg": "ES256" - }, - "signature": "dV1paJ3Ck1Ph4FcEhg_frjqxdlGdI6-ywRamk6CvMOcaOEUdCWCpCPQeBQpD2N6tGjkoG1BbstkFNflllfenCw", - "protected": "eyJmb3JtYXRMZW5ndGgiOjU0NzgsImZvcm1hdFRhaWwiOiJDbjAiLCJ0aW1lIjoiMjAxNi0wNC0xOFQyMDo1NDo0MloifQ" - } - ] -} \ No newline at end of file diff --git a/manifest/fixtures/v2s1.manifest.json b/manifest/fixtures/v2s1.manifest.json new file mode 120000 index 0000000000..8021e76f01 --- /dev/null +++ b/manifest/fixtures/v2s1.manifest.json @@ -0,0 +1 @@ +../../internal/manifest/testdata/v2s1.manifest.json \ No newline at end of file diff --git a/manifest/fixtures/v2s2.manifest.json b/manifest/fixtures/v2s2.manifest.json deleted file mode 100644 index 198da23f92..0000000000 --- a/manifest/fixtures/v2s2.manifest.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "schemaVersion": 2, - "mediaType": "application/vnd.docker.distribution.manifest.v2+json", - "config": { - "mediaType": "application/vnd.docker.container.image.v1+json", - "size": 7023, - "digest": "sha256:b5b2b2c507a0944348e0303114d8d93aaaa081732b86451d9bce1f432a537bc7" - }, - "layers": [ - { - "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", - "size": 32654, - "digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f" - }, - { - "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", - "size": 16724, - "digest": "sha256:3c3a4604a545cdc127456d94e421cd355bca5b528f4a9c1905b15da2eb4a4c6b" - }, - { - "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", - "size": 73109, - "digest": "sha256:ec4b8955958665577945c89419d1af06b5f7636b4ac3da7f12184802ad867736" - } - ] -} \ No newline at end of file diff --git a/manifest/fixtures/v2s2.manifest.json b/manifest/fixtures/v2s2.manifest.json new file mode 120000 index 0000000000..f172a45c59 --- /dev/null +++ b/manifest/fixtures/v2s2.manifest.json @@ -0,0 +1 @@ +../../internal/manifest/testdata/v2s2.manifest.json \ No newline at end of file diff --git a/manifest/fixtures/v2s2nomime.manifest.json b/manifest/fixtures/v2s2nomime.manifest.json deleted file mode 100644 index a0b06c233b..0000000000 --- a/manifest/fixtures/v2s2nomime.manifest.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "schemaVersion": 2, - "config": { - "mediaType": "application/vnd.docker.container.image.v1+json", - "size": 7023, - "digest": "sha256:b5b2b2c507a0944348e0303114d8d93aaaa081732b86451d9bce1f432a537bc7" - }, - "layers": [ - ] -} diff --git a/manifest/fixtures/v2s2nomime.manifest.json b/manifest/fixtures/v2s2nomime.manifest.json new file mode 120000 index 0000000000..bf022a4426 --- /dev/null +++ b/manifest/fixtures/v2s2nomime.manifest.json @@ -0,0 +1 @@ +../../internal/manifest/testdata/v2s2nomime.manifest.json \ No newline at end of file diff --git a/manifest/list.go b/manifest/list.go index 58982597e6..1d6fdc9f56 100644 --- a/manifest/list.go +++ b/manifest/list.go @@ -1,10 +1,7 @@ package manifest import ( - "fmt" - - "github.com/containers/image/v5/types" - digest "github.com/opencontainers/go-digest" + "github.com/containers/image/v5/internal/manifest" imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" ) @@ -21,56 +18,14 @@ var ( // Callers can either use this abstract interface without understanding the details of the formats, // or instantiate a specific implementation (e.g. manifest.OCI1Index) and access the public members // directly. -type List interface { - // MIMEType returns the MIME type of this particular manifest list. - MIMEType() string - - // Instances returns a list of the manifests that this list knows of, other than its own. - Instances() []digest.Digest - - // Update information about the list's instances. The length of the passed-in slice must - // match the length of the list of instances which the list already contains, and every field - // must be specified. - UpdateInstances([]ListUpdate) error - - // Instance returns the size and MIME type of a particular instance in the list. - Instance(digest.Digest) (ListUpdate, error) - - // ChooseInstance selects which manifest is most appropriate for the platform described by the - // SystemContext, or for the current platform if the SystemContext doesn't specify any details. - ChooseInstance(ctx *types.SystemContext) (digest.Digest, error) - - // Serialize returns the list in a blob format. - // NOTE: Serialize() does not in general reproduce the original blob if this object was loaded - // from, even if no modifications were made! - Serialize() ([]byte, error) - - // ConvertToMIMEType returns the list rebuilt to the specified MIME type, or an error. - ConvertToMIMEType(mimeType string) (List, error) - - // Clone returns a deep copy of this list and its contents. - Clone() List -} +type List = manifest.ListPublic // ListUpdate includes the fields which a List's UpdateInstances() method will modify. -type ListUpdate struct { - Digest digest.Digest - Size int64 - MediaType string -} +type ListUpdate = manifest.ListUpdate // ListFromBlob parses a list of manifests. -func ListFromBlob(manifest []byte, manifestMIMEType string) (List, error) { - normalized := NormalizedMIMEType(manifestMIMEType) - switch normalized { - case DockerV2ListMediaType: - return Schema2ListFromManifest(manifest) - case imgspecv1.MediaTypeImageIndex: - return OCI1IndexFromManifest(manifest) - case DockerV2Schema1MediaType, DockerV2Schema1SignedMediaType, imgspecv1.MediaTypeImageManifest, DockerV2Schema2MediaType: - return nil, fmt.Errorf("Treating single images as manifest lists is not implemented") - } - return nil, fmt.Errorf("Unimplemented manifest list MIME type %s (normalized as %s)", manifestMIMEType, normalized) +func ListFromBlob(manifestBlob []byte, manifestMIMEType string) (List, error) { + return manifest.ListPublicFromBlob(manifestBlob, manifestMIMEType) } // ConvertListToMIMEType converts the passed-in manifest list to a manifest diff --git a/manifest/list_test.go b/manifest/list_test.go index 540836d2a3..77976cfa8d 100644 --- a/manifest/list_test.go +++ b/manifest/list_test.go @@ -6,6 +6,7 @@ import ( "path/filepath" "testing" + "github.com/containers/image/v5/internal/manifest" "github.com/containers/image/v5/types" digest "github.com/opencontainers/go-digest" imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" @@ -14,10 +15,10 @@ import ( ) func pare(m List) { - if impl, ok := m.(*OCI1Index); ok { + if impl, ok := m.(*manifest.OCI1Index); ok { impl.Annotations = nil } - if impl, ok := m.(*Schema2List); ok { + if impl, ok := m.(*manifest.Schema2List); ok { for i := range impl.Manifests { impl.Manifests[i].Platform.Features = nil } diff --git a/manifest/manifest.go b/manifest/manifest.go index 53fc866a78..959aac935e 100644 --- a/manifest/manifest.go +++ b/manifest/manifest.go @@ -1,10 +1,9 @@ package manifest import ( - "encoding/json" "fmt" - internalManifest "github.com/containers/image/v5/internal/manifest" + "github.com/containers/image/v5/internal/manifest" "github.com/containers/image/v5/types" "github.com/containers/libtrust" digest "github.com/opencontainers/go-digest" @@ -16,28 +15,28 @@ import ( // FIXME(runcom, mitr): should we have a mediatype pkg?? const ( // DockerV2Schema1MediaType MIME type represents Docker manifest schema 1 - DockerV2Schema1MediaType = "application/vnd.docker.distribution.manifest.v1+json" + DockerV2Schema1MediaType = manifest.DockerV2Schema1MediaType // DockerV2Schema1MediaType MIME type represents Docker manifest schema 1 with a JWS signature - DockerV2Schema1SignedMediaType = "application/vnd.docker.distribution.manifest.v1+prettyjws" + DockerV2Schema1SignedMediaType = manifest.DockerV2Schema1SignedMediaType // DockerV2Schema2MediaType MIME type represents Docker manifest schema 2 - DockerV2Schema2MediaType = "application/vnd.docker.distribution.manifest.v2+json" + DockerV2Schema2MediaType = manifest.DockerV2Schema2MediaType // DockerV2Schema2ConfigMediaType is the MIME type used for schema 2 config blobs. - DockerV2Schema2ConfigMediaType = "application/vnd.docker.container.image.v1+json" + DockerV2Schema2ConfigMediaType = manifest.DockerV2Schema2ConfigMediaType // DockerV2Schema2LayerMediaType is the MIME type used for schema 2 layers. - DockerV2Schema2LayerMediaType = "application/vnd.docker.image.rootfs.diff.tar.gzip" + DockerV2Schema2LayerMediaType = manifest.DockerV2Schema2LayerMediaType // DockerV2SchemaLayerMediaTypeUncompressed is the mediaType used for uncompressed layers. - DockerV2SchemaLayerMediaTypeUncompressed = "application/vnd.docker.image.rootfs.diff.tar" + DockerV2SchemaLayerMediaTypeUncompressed = manifest.DockerV2SchemaLayerMediaTypeUncompressed // DockerV2ListMediaType MIME type represents Docker manifest schema 2 list - DockerV2ListMediaType = "application/vnd.docker.distribution.manifest.list.v2+json" + DockerV2ListMediaType = manifest.DockerV2ListMediaType // DockerV2Schema2ForeignLayerMediaType is the MIME type used for schema 2 foreign layers. - DockerV2Schema2ForeignLayerMediaType = "application/vnd.docker.image.rootfs.foreign.diff.tar" + DockerV2Schema2ForeignLayerMediaType = manifest.DockerV2Schema2ForeignLayerMediaType // DockerV2Schema2ForeignLayerMediaType is the MIME type used for gzipped schema 2 foreign layers. - DockerV2Schema2ForeignLayerMediaTypeGzip = "application/vnd.docker.image.rootfs.foreign.diff.tar.gzip" + DockerV2Schema2ForeignLayerMediaTypeGzip = manifest.DockerV2Schema2ForeignLayerMediaTypeGzip ) // NonImageArtifactError (detected via errors.As) is used when asking for an image-specific operation // on an object which is not a “container image” in the standard sense (e.g. an OCI artifact) -type NonImageArtifactError = internalManifest.NonImageArtifactError +type NonImageArtifactError = manifest.NonImageArtifactError // SupportedSchema2MediaType checks if the specified string is a supported Docker v2s2 media type. func SupportedSchema2MediaType(m string) error { @@ -102,102 +101,21 @@ type LayerInfo struct { // GuessMIMEType guesses MIME type of a manifest and returns it _if it is recognized_, or "" if unknown or unrecognized. // FIXME? We should, in general, prefer out-of-band MIME type instead of blindly parsing the manifest, // but we may not have such metadata available (e.g. when the manifest is a local file). -func GuessMIMEType(manifest []byte) string { - // A subset of manifest fields; the rest is silently ignored by json.Unmarshal. - // Also docker/distribution/manifest.Versioned. - meta := struct { - MediaType string `json:"mediaType"` - SchemaVersion int `json:"schemaVersion"` - Signatures interface{} `json:"signatures"` - }{} - if err := json.Unmarshal(manifest, &meta); err != nil { - return "" - } - - switch meta.MediaType { - case DockerV2Schema2MediaType, DockerV2ListMediaType, - imgspecv1.MediaTypeImageManifest, imgspecv1.MediaTypeImageIndex: // A recognized type. - return meta.MediaType - } - // this is the only way the function can return DockerV2Schema1MediaType, and recognizing that is essential for stripping the JWS signatures = computing the correct manifest digest. - switch meta.SchemaVersion { - case 1: - if meta.Signatures != nil { - return DockerV2Schema1SignedMediaType - } - return DockerV2Schema1MediaType - case 2: - // Best effort to understand if this is an OCI image since mediaType - // wasn't in the manifest for OCI image-spec < 1.0.2. - // For docker v2s2 meta.MediaType should have been set. But given the data, this is our best guess. - ociMan := struct { - Config struct { - MediaType string `json:"mediaType"` - } `json:"config"` - }{} - if err := json.Unmarshal(manifest, &ociMan); err != nil { - return "" - } - switch ociMan.Config.MediaType { - case imgspecv1.MediaTypeImageConfig: - return imgspecv1.MediaTypeImageManifest - case DockerV2Schema2ConfigMediaType: - // This case should not happen since a Docker image - // must declare a top-level media type and - // `meta.MediaType` has already been checked. - return DockerV2Schema2MediaType - } - // Maybe an image index or an OCI artifact. - ociIndex := struct { - Manifests []imgspecv1.Descriptor `json:"manifests"` - }{} - if err := json.Unmarshal(manifest, &ociIndex); err != nil { - return "" - } - if len(ociIndex.Manifests) != 0 { - if ociMan.Config.MediaType == "" { - return imgspecv1.MediaTypeImageIndex - } - // FIXME: this is mixing media types of manifests and configs. - return ociMan.Config.MediaType - } - // It's most likely an OCI artifact with a custom config media - // type which is not (and cannot) be covered by the media-type - // checks cabove. - return imgspecv1.MediaTypeImageManifest - } - return "" +func GuessMIMEType(manifestBlob []byte) string { + return manifest.GuessMIMEType(manifestBlob) } // Digest returns the a digest of a docker manifest, with any necessary implied transformations like stripping v1s1 signatures. -func Digest(manifest []byte) (digest.Digest, error) { - if GuessMIMEType(manifest) == DockerV2Schema1SignedMediaType { - sig, err := libtrust.ParsePrettySignature(manifest, "signatures") - if err != nil { - return "", err - } - manifest, err = sig.Payload() - if err != nil { - // Coverage: This should never happen, libtrust's Payload() can fail only if joseBase64UrlDecode() fails, on a string - // that libtrust itself has josebase64UrlEncode()d - return "", err - } - } - - return digest.FromBytes(manifest), nil +func Digest(manifestBlob []byte) (digest.Digest, error) { + return manifest.Digest(manifestBlob) } // MatchesDigest returns true iff the manifest matches expectedDigest. // Error may be set if this returns false. // Note that this is not doing ConstantTimeCompare; by the time we get here, the cryptographic signature must already have been verified, // or we are not using a cryptographic channel and the attacker can modify the digest along with the manifest blob. -func MatchesDigest(manifest []byte, expectedDigest digest.Digest) (bool, error) { - // This should eventually support various digest types. - actualDigest, err := Digest(manifest) - if err != nil { - return false, err - } - return expectedDigest == actualDigest, nil +func MatchesDigest(manifestBlob []byte, expectedDigest digest.Digest) (bool, error) { + return manifest.MatchesDigest(manifestBlob, expectedDigest) } // AddDummyV2S1Signature adds an JWS signature with a temporary key (i.e. useless) to a v2s1 manifest. @@ -231,30 +149,7 @@ func MIMETypeSupportsEncryption(mimeType string) bool { // NormalizedMIMEType returns the effective MIME type of a manifest MIME type returned by a server, // centralizing various workarounds. func NormalizedMIMEType(input string) string { - switch input { - // "application/json" is a valid v2s1 value per https://github.com/docker/distribution/blob/master/docs/spec/manifest-v2-1.md . - // This works for now, when nothing else seems to return "application/json"; if that were not true, the mapping/detection might - // need to happen within the ImageSource. - case "application/json": - return DockerV2Schema1SignedMediaType - case DockerV2Schema1MediaType, DockerV2Schema1SignedMediaType, - imgspecv1.MediaTypeImageManifest, - imgspecv1.MediaTypeImageIndex, - DockerV2Schema2MediaType, - DockerV2ListMediaType: - return input - default: - // If it's not a recognized manifest media type, or we have failed determining the type, we'll try one last time - // to deserialize using v2s1 as per https://github.com/docker/distribution/blob/master/manifests.go#L108 - // and https://github.com/docker/distribution/blob/master/manifest/schema1/manifest.go#L50 - // - // Crane registries can also return "text/plain", or pretty much anything else depending on a file extension “recognized” in the tag. - // This makes no real sense, but it happens - // because requests for manifests are - // redirected to a content distribution - // network which is configured that way. See https://bugzilla.redhat.com/show_bug.cgi?id=1389442 - return DockerV2Schema1SignedMediaType - } + return manifest.NormalizedMIMEType(input) } // FromBlob returns a Manifest instance for the specified manifest blob and the corresponding MIME type diff --git a/manifest/oci.go b/manifest/oci.go index 0674eb47b8..eb23547680 100644 --- a/manifest/oci.go +++ b/manifest/oci.go @@ -5,13 +5,14 @@ import ( "fmt" "strings" - internalManifest "github.com/containers/image/v5/internal/manifest" + "github.com/containers/image/v5/internal/manifest" compressiontypes "github.com/containers/image/v5/pkg/compression/types" "github.com/containers/image/v5/types" ociencspec "github.com/containers/ocicrypt/spec" "github.com/opencontainers/go-digest" "github.com/opencontainers/image-spec/specs-go" imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" + "golang.org/x/exp/slices" ) // BlobInfoFromOCI1Descriptor returns a types.BlobInfo based on the input OCI1 descriptor. @@ -49,13 +50,13 @@ func SupportedOCI1MediaType(m string) error { } // OCI1FromManifest creates an OCI1 manifest instance from a manifest blob. -func OCI1FromManifest(manifest []byte) (*OCI1, error) { +func OCI1FromManifest(manifestBlob []byte) (*OCI1, error) { oci1 := OCI1{} - if err := json.Unmarshal(manifest, &oci1); err != nil { + if err := json.Unmarshal(manifestBlob, &oci1); err != nil { return nil, err } - if err := validateUnambiguousManifestFormat(manifest, imgspecv1.MediaTypeImageIndex, - allowedFieldConfig|allowedFieldLayers); err != nil { + if err := manifest.ValidateUnambiguousManifestFormat(manifestBlob, imgspecv1.MediaTypeImageIndex, + manifest.AllowedFieldConfig|manifest.AllowedFieldLayers); err != nil { return nil, err } return &oci1, nil @@ -160,10 +161,8 @@ func (m *OCI1) UpdateLayerInfos(layerInfos []types.BlobInfo) error { // getEncryptedMediaType will return the mediatype to its encrypted counterpart and return // an error if the mediatype does not support encryption func getEncryptedMediaType(mediatype string) (string, error) { - for _, s := range strings.Split(mediatype, "+")[1:] { - if s == "encrypted" { - return "", fmt.Errorf("unsupported mediaType: %v already encrypted", mediatype) - } + if slices.Contains(strings.Split(mediatype, "+")[1:], "encrypted") { + return "", fmt.Errorf("unsupported mediaType: %v already encrypted", mediatype) } unsuffixedMediatype := strings.Split(mediatype, "+")[0] switch unsuffixedMediatype { @@ -178,7 +177,7 @@ func getEncryptedMediaType(mediatype string) (string, error) { // an error if the mediatype does not support decryption func getDecryptedMediaType(mediatype string) (string, error) { if !strings.HasSuffix(mediatype, "+encrypted") { - return "", fmt.Errorf("unsupported mediaType to decrypt %v:", mediatype) + return "", fmt.Errorf("unsupported mediaType to decrypt: %v", mediatype) } return strings.TrimSuffix(mediatype, "+encrypted"), nil @@ -197,7 +196,7 @@ func (m *OCI1) Inspect(configGetter func(types.BlobInfo) ([]byte, error)) (*type // Most software calling this without human intervention is going to expect the values to be realistic and relevant, // and is probably better served by failing; we can always re-visit that later if we fail now, but // if we started returning some data for OCI artifacts now, we couldn’t start failing in this function later. - return nil, internalManifest.NewNonImageArtifactError(m.Config.MediaType) + return nil, manifest.NewNonImageArtifactError(m.Config.MediaType) } config, err := configGetter(m.ConfigInfo()) @@ -219,6 +218,7 @@ func (m *OCI1) Inspect(configGetter func(types.BlobInfo) ([]byte, error)) (*type DockerVersion: d1.DockerVersion, Labels: v1.Config.Labels, Architecture: v1.Architecture, + Variant: v1.Variant, Os: v1.OS, Layers: layerInfosToStrings(layerInfos), LayersData: imgInspectLayersFromLayerInfos(layerInfos), @@ -247,7 +247,7 @@ func (m *OCI1) ImageID([]digest.Digest) (string, error) { // (The only known caller of ImageID is storage/storageImageDestination.computeID, // which can’t work with non-image artifacts.) if m.Config.MediaType != imgspecv1.MediaTypeImageConfig { - return "", internalManifest.NewNonImageArtifactError(m.Config.MediaType) + return "", manifest.NewNonImageArtifactError(m.Config.MediaType) } if err := m.Config.Digest.Validate(); err != nil { diff --git a/manifest/oci_index.go b/manifest/oci_index.go index 726207b9d4..193b08935a 100644 --- a/manifest/oci_index.go +++ b/manifest/oci_index.go @@ -1,232 +1,27 @@ package manifest import ( - "encoding/json" - "fmt" - "runtime" - - platform "github.com/containers/image/v5/internal/pkg/platform" - "github.com/containers/image/v5/types" - "github.com/opencontainers/go-digest" - imgspec "github.com/opencontainers/image-spec/specs-go" + "github.com/containers/image/v5/internal/manifest" imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" ) // OCI1Index is just an alias for the OCI index type, but one which we can // provide methods for. -type OCI1Index struct { - imgspecv1.Index -} - -// MIMEType returns the MIME type of this particular manifest index. -func (index *OCI1Index) MIMEType() string { - return imgspecv1.MediaTypeImageIndex -} - -// Instances returns a slice of digests of the manifests that this index knows of. -func (index *OCI1Index) Instances() []digest.Digest { - results := make([]digest.Digest, len(index.Manifests)) - for i, m := range index.Manifests { - results[i] = m.Digest - } - return results -} - -// Instance returns the ListUpdate of a particular instance in the index. -func (index *OCI1Index) Instance(instanceDigest digest.Digest) (ListUpdate, error) { - for _, manifest := range index.Manifests { - if manifest.Digest == instanceDigest { - return ListUpdate{ - Digest: manifest.Digest, - Size: manifest.Size, - MediaType: manifest.MediaType, - }, nil - } - } - return ListUpdate{}, fmt.Errorf("unable to find instance %s in OCI1Index", instanceDigest) -} - -// UpdateInstances updates the sizes, digests, and media types of the manifests -// which the list catalogs. -func (index *OCI1Index) UpdateInstances(updates []ListUpdate) error { - if len(updates) != len(index.Manifests) { - return fmt.Errorf("incorrect number of update entries passed to OCI1Index.UpdateInstances: expected %d, got %d", len(index.Manifests), len(updates)) - } - for i := range updates { - if err := updates[i].Digest.Validate(); err != nil { - return fmt.Errorf("update %d of %d passed to OCI1Index.UpdateInstances contained an invalid digest: %w", i+1, len(updates), err) - } - index.Manifests[i].Digest = updates[i].Digest - if updates[i].Size < 0 { - return fmt.Errorf("update %d of %d passed to OCI1Index.UpdateInstances had an invalid size (%d)", i+1, len(updates), updates[i].Size) - } - index.Manifests[i].Size = updates[i].Size - if updates[i].MediaType == "" { - return fmt.Errorf("update %d of %d passed to OCI1Index.UpdateInstances had no media type (was %q)", i+1, len(updates), index.Manifests[i].MediaType) - } - index.Manifests[i].MediaType = updates[i].MediaType - } - return nil -} - -// ChooseInstance parses blob as an oci v1 manifest index, and returns the digest -// of the image which is appropriate for the current environment. -func (index *OCI1Index) ChooseInstance(ctx *types.SystemContext) (digest.Digest, error) { - wantedPlatforms, err := platform.WantedPlatforms(ctx) - if err != nil { - return "", fmt.Errorf("getting platform information %#v: %w", ctx, err) - } - for _, wantedPlatform := range wantedPlatforms { - for _, d := range index.Manifests { - if d.Platform == nil { - continue - } - imagePlatform := imgspecv1.Platform{ - Architecture: d.Platform.Architecture, - OS: d.Platform.OS, - OSVersion: d.Platform.OSVersion, - OSFeatures: dupStringSlice(d.Platform.OSFeatures), - Variant: d.Platform.Variant, - } - if platform.MatchesPlatform(imagePlatform, wantedPlatform) { - return d.Digest, nil - } - } - } - - for _, d := range index.Manifests { - if d.Platform == nil { - return d.Digest, nil - } - } - return "", fmt.Errorf("no image found in image index for architecture %s, variant %q, OS %s", wantedPlatforms[0].Architecture, wantedPlatforms[0].Variant, wantedPlatforms[0].OS) -} - -// Serialize returns the index in a blob format. -// NOTE: Serialize() does not in general reproduce the original blob if this object was loaded from one, even if no modifications were made! -func (index *OCI1Index) Serialize() ([]byte, error) { - buf, err := json.Marshal(index) - if err != nil { - return nil, fmt.Errorf("marshaling OCI1Index %#v: %w", index, err) - } - return buf, nil -} +type OCI1Index = manifest.OCI1IndexPublic // OCI1IndexFromComponents creates an OCI1 image index instance from the // supplied data. func OCI1IndexFromComponents(components []imgspecv1.Descriptor, annotations map[string]string) *OCI1Index { - index := OCI1Index{ - imgspecv1.Index{ - Versioned: imgspec.Versioned{SchemaVersion: 2}, - MediaType: imgspecv1.MediaTypeImageIndex, - Manifests: make([]imgspecv1.Descriptor, len(components)), - Annotations: dupStringStringMap(annotations), - }, - } - for i, component := range components { - var platform *imgspecv1.Platform - if component.Platform != nil { - platform = &imgspecv1.Platform{ - Architecture: component.Platform.Architecture, - OS: component.Platform.OS, - OSVersion: component.Platform.OSVersion, - OSFeatures: dupStringSlice(component.Platform.OSFeatures), - Variant: component.Platform.Variant, - } - } - m := imgspecv1.Descriptor{ - MediaType: component.MediaType, - Size: component.Size, - Digest: component.Digest, - URLs: dupStringSlice(component.URLs), - Annotations: dupStringStringMap(component.Annotations), - Platform: platform, - } - index.Manifests[i] = m - } - return &index + return manifest.OCI1IndexPublicFromComponents(components, annotations) } // OCI1IndexClone creates a deep copy of the passed-in index. func OCI1IndexClone(index *OCI1Index) *OCI1Index { - return OCI1IndexFromComponents(index.Manifests, index.Annotations) -} - -// ToOCI1Index returns the index encoded as an OCI1 index. -func (index *OCI1Index) ToOCI1Index() (*OCI1Index, error) { - return OCI1IndexClone(index), nil -} - -// ToSchema2List returns the index encoded as a Schema2 list. -func (index *OCI1Index) ToSchema2List() (*Schema2List, error) { - components := make([]Schema2ManifestDescriptor, 0, len(index.Manifests)) - for _, manifest := range index.Manifests { - platform := manifest.Platform - if platform == nil { - platform = &imgspecv1.Platform{ - OS: runtime.GOOS, - Architecture: runtime.GOARCH, - } - } - converted := Schema2ManifestDescriptor{ - Schema2Descriptor{ - MediaType: manifest.MediaType, - Size: manifest.Size, - Digest: manifest.Digest, - URLs: dupStringSlice(manifest.URLs), - }, - Schema2PlatformSpec{ - OS: platform.OS, - Architecture: platform.Architecture, - OSFeatures: dupStringSlice(platform.OSFeatures), - OSVersion: platform.OSVersion, - Variant: platform.Variant, - }, - } - components = append(components, converted) - } - s2 := Schema2ListFromComponents(components) - return s2, nil + return manifest.OCI1IndexPublicClone(index) } // OCI1IndexFromManifest creates an OCI1 manifest index instance from marshalled // JSON, presumably generated by encoding a OCI1 manifest index. -func OCI1IndexFromManifest(manifest []byte) (*OCI1Index, error) { - index := OCI1Index{ - Index: imgspecv1.Index{ - Versioned: imgspec.Versioned{SchemaVersion: 2}, - MediaType: imgspecv1.MediaTypeImageIndex, - Manifests: []imgspecv1.Descriptor{}, - Annotations: make(map[string]string), - }, - } - if err := json.Unmarshal(manifest, &index); err != nil { - return nil, fmt.Errorf("unmarshaling OCI1Index %q: %w", string(manifest), err) - } - if err := validateUnambiguousManifestFormat(manifest, imgspecv1.MediaTypeImageIndex, - allowedFieldManifests); err != nil { - return nil, err - } - return &index, nil -} - -// Clone returns a deep copy of this list and its contents. -func (index *OCI1Index) Clone() List { - return OCI1IndexClone(index) -} - -// ConvertToMIMEType converts the passed-in image index to a manifest list of -// the specified type. -func (index *OCI1Index) ConvertToMIMEType(manifestMIMEType string) (List, error) { - switch normalized := NormalizedMIMEType(manifestMIMEType); normalized { - case DockerV2ListMediaType: - return index.ToSchema2List() - case imgspecv1.MediaTypeImageIndex: - return index.Clone(), nil - case DockerV2Schema1MediaType, DockerV2Schema1SignedMediaType, imgspecv1.MediaTypeImageManifest, DockerV2Schema2MediaType: - return nil, fmt.Errorf("Can not convert image index to MIME type %q, which is not a list type", manifestMIMEType) - default: - // Note that this may not be reachable, NormalizedMIMEType has a default for unknown values. - return nil, fmt.Errorf("Unimplemented manifest MIME type %s", manifestMIMEType) - } +func OCI1IndexFromManifest(manifestBlob []byte) (*OCI1Index, error) { + return manifest.OCI1IndexPublicFromManifest(manifestBlob) } diff --git a/oci/archive/oci_src.go b/oci/archive/oci_src.go index e5ad2570ef..6c9ee33402 100644 --- a/oci/archive/oci_src.go +++ b/oci/archive/oci_src.go @@ -17,6 +17,17 @@ import ( "github.com/sirupsen/logrus" ) +// ImageNotFoundError is used when the OCI structure, in principle, exists and seems valid enough, +// but nothing matches the “image” part of the provided reference. +type ImageNotFoundError struct { + ref ociArchiveReference + // We may make members public, or add methods, in the future. +} + +func (e ImageNotFoundError) Error() string { + return fmt.Sprintf("no descriptor found for reference %q", e.ref.image) +} + type ociArchiveImageSource struct { impl.Compat @@ -35,6 +46,10 @@ func newImageSource(ctx context.Context, sys *types.SystemContext, ref ociArchiv unpackedSrc, err := tempDirRef.ociRefExtracted.NewImageSource(ctx, sys) if err != nil { + var notFound ocilayout.ImageNotFoundError + if errors.As(err, ¬Found) { + err = ImageNotFoundError{ref: ref} + } if err := tempDirRef.deleteTempDir(); err != nil { return nil, fmt.Errorf("deleting temp directory %q: %w", tempDirRef.tempDirectory, err) } diff --git a/oci/internal/oci_util.go b/oci/internal/oci_util.go index 148bc12fa3..53827b11af 100644 --- a/oci/internal/oci_util.go +++ b/oci/internal/oci_util.go @@ -58,13 +58,7 @@ func splitPathAndImageWindows(reference string) (string, string) { } func splitPathAndImageNonWindows(reference string) (string, string) { - sep := strings.SplitN(reference, ":", 2) - path := sep[0] - - var image string - if len(sep) == 2 { - image = sep[1] - } + path, image, _ := strings.Cut(reference, ":") // image is set to "" if there is no ":" return path, image } diff --git a/oci/internal/oci_util_test.go b/oci/internal/oci_util_test.go index b10071d2d7..2438acf1d3 100644 --- a/oci/internal/oci_util_test.go +++ b/oci/internal/oci_util_test.go @@ -2,8 +2,9 @@ package internal import ( "fmt" - "github.com/stretchr/testify/assert" "testing" + + "github.com/stretchr/testify/assert" ) type testDataSplitReference struct { diff --git a/oci/layout/oci_dest.go b/oci/layout/oci_dest.go index 4face7213c..4e4433f12b 100644 --- a/oci/layout/oci_dest.go +++ b/oci/layout/oci_dest.go @@ -12,9 +12,9 @@ import ( "github.com/containers/image/v5/internal/imagedestination/impl" "github.com/containers/image/v5/internal/imagedestination/stubs" + "github.com/containers/image/v5/internal/manifest" "github.com/containers/image/v5/internal/private" "github.com/containers/image/v5/internal/putblobdigest" - "github.com/containers/image/v5/manifest" "github.com/containers/image/v5/types" digest "github.com/opencontainers/go-digest" imgspec "github.com/opencontainers/image-spec/specs-go" diff --git a/oci/layout/oci_src.go b/oci/layout/oci_src.go index b2d963b019..817a4e40d0 100644 --- a/oci/layout/oci_src.go +++ b/oci/layout/oci_src.go @@ -12,8 +12,8 @@ import ( "github.com/containers/image/v5/internal/imagesource/impl" "github.com/containers/image/v5/internal/imagesource/stubs" + "github.com/containers/image/v5/internal/manifest" "github.com/containers/image/v5/internal/private" - "github.com/containers/image/v5/manifest" "github.com/containers/image/v5/pkg/tlsclientconfig" "github.com/containers/image/v5/types" "github.com/docker/go-connections/tlsconfig" @@ -21,6 +21,17 @@ import ( imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" ) +// ImageNotFoundError is used when the OCI structure, in principle, exists and seems valid enough, +// but nothing matches the “image” part of the provided reference. +type ImageNotFoundError struct { + ref ociReference + // We may make members public, or add methods, in the future. +} + +func (e ImageNotFoundError) Error() string { + return fmt.Sprintf("no descriptor found for reference %q", e.ref.image) +} + type ociImageSource struct { impl.Compat impl.PropertyMethodsInitialize @@ -96,7 +107,7 @@ func (s *ociImageSource) GetManifest(ctx context.Context, instanceDigest *digest var err error if instanceDigest == nil { - dig = digest.Digest(s.descriptor.Digest) + dig = s.descriptor.Digest mimeType = s.descriptor.MediaType } else { dig = *instanceDigest diff --git a/oci/layout/oci_src_test.go b/oci/layout/oci_src_test.go index b52e64e38b..be5145567c 100644 --- a/oci/layout/oci_src_test.go +++ b/oci/layout/oci_src_test.go @@ -29,7 +29,7 @@ var httpServerAddr string func TestMain(m *testing.M) { httpServer, err := startRemoteLayerServer() if err != nil { - println("Error starting test TLS server", err.Error()) + fmt.Fprintf(os.Stderr, "Error starting test TLS server: %v", err.Error()) os.Exit(1) } diff --git a/oci/layout/oci_transport.go b/oci/layout/oci_transport.go index be22bed6d5..4a4ab9b2c6 100644 --- a/oci/layout/oci_transport.go +++ b/oci/layout/oci_transport.go @@ -100,7 +100,7 @@ func (ref ociReference) Transport() types.ImageTransport { // StringWithinTransport returns a string representation of the reference, which MUST be such that // reference.Transport().ParseReference(reference.StringWithinTransport()) returns an equivalent reference. // NOTE: The returned string is not promised to be equal to the original input to ParseReference; -// e.g. default attribute values omitted by the user may be filled in in the return value, or vice versa. +// e.g. default attribute values omitted by the user may be filled in the return value, or vice versa. // WARNING: Do not use the return value in the UI to describe an image, it does not contain the Transport().Name() prefix. func (ref ociReference) StringWithinTransport() string { return fmt.Sprintf("%s:%s", ref.dir, ref.image) @@ -179,35 +179,25 @@ func (ref ociReference) getManifestDescriptor() (imgspecv1.Descriptor, error) { return imgspecv1.Descriptor{}, err } - var d *imgspecv1.Descriptor if ref.image == "" { // return manifest if only one image is in the oci directory - if len(index.Manifests) == 1 { - d = &index.Manifests[0] - } else { + if len(index.Manifests) != 1 { // ask user to choose image when more than one image in the oci directory return imgspecv1.Descriptor{}, ErrMoreThanOneImage } + return index.Manifests[0], nil } else { // if image specified, look through all manifests for a match for _, md := range index.Manifests { if md.MediaType != imgspecv1.MediaTypeImageManifest && md.MediaType != imgspecv1.MediaTypeImageIndex { continue } - refName, ok := md.Annotations[imgspecv1.AnnotationRefName] - if !ok { - continue - } - if refName == ref.image { - d = &md - break + if refName, ok := md.Annotations[imgspecv1.AnnotationRefName]; ok && refName == ref.image { + return md, nil } } } - if d == nil { - return imgspecv1.Descriptor{}, fmt.Errorf("no descriptor found for reference %q", ref.image) - } - return *d, nil + return imgspecv1.Descriptor{}, ImageNotFoundError{ref} } // LoadManifestDescriptor loads the manifest descriptor to be used to retrieve the image name diff --git a/openshift/openshift-copies.go b/openshift/openshift-copies.go index 42e8970a07..0b737f0204 100644 --- a/openshift/openshift-copies.go +++ b/openshift/openshift-copies.go @@ -3,11 +3,12 @@ package openshift import ( "crypto/tls" "crypto/x509" - "encoding/json" + "encoding/base64" "errors" "fmt" "net" "net/http" + "net/netip" "net/url" "os" "path" @@ -17,10 +18,10 @@ import ( "time" "github.com/containers/storage/pkg/homedir" - "github.com/ghodss/yaml" "github.com/imdario/mergo" "github.com/sirupsen/logrus" - "golang.org/x/net/http2" + "golang.org/x/exp/slices" + "gopkg.in/yaml.v3" ) // restTLSClientConfig is a modified copy of k8s.io/kubernetes/pkg/client/restclient.TLSClientConfig. @@ -205,15 +206,12 @@ func (config *directClientConfig) ClientConfig() (*restConfig, error) { if isConfigTransportTLS(*clientConfig) { var err error // REMOVED: Support for interactive fallback. - userAuthPartialConfig, err := getUserIdentificationPartialConfig(configAuthInfo) - if err != nil { - return nil, err - } + userAuthPartialConfig := getUserIdentificationPartialConfig(configAuthInfo) if err = mergo.MergeWithOverwrite(clientConfig, userAuthPartialConfig); err != nil { return nil, err } - serverAuthPartialConfig, err := getServerIdentificationPartialConfig(configAuthInfo, configClusterInfo) + serverAuthPartialConfig, err := getServerIdentificationPartialConfig(configClusterInfo) if err != nil { return nil, err } @@ -232,7 +230,7 @@ func (config *directClientConfig) ClientConfig() (*restConfig, error) { // 1. configClusterInfo (the final result of command line flags and merged .kubeconfig files) // 2. configAuthInfo.auth-path (this file can contain information that conflicts with #1, and we want #1 to win the priority) // 3. load the ~/.kubernetes_auth file as a default -func getServerIdentificationPartialConfig(configAuthInfo clientcmdAuthInfo, configClusterInfo clientcmdCluster) (*restConfig, error) { +func getServerIdentificationPartialConfig(configClusterInfo clientcmdCluster) (*restConfig, error) { mergedConfig := &restConfig{} // configClusterInfo holds the information identify the server provided by .kubeconfig @@ -255,7 +253,7 @@ func getServerIdentificationPartialConfig(configAuthInfo clientcmdAuthInfo, conf // 2. configAuthInfo.auth-path (this file can contain information that conflicts with #1, and we want #1 to win the priority) // 3. if there is not enough information to identify the user, load try the ~/.kubernetes_auth file // 4. if there is not enough information to identify the user, prompt if possible -func getUserIdentificationPartialConfig(configAuthInfo clientcmdAuthInfo) (*restConfig, error) { +func getUserIdentificationPartialConfig(configAuthInfo clientcmdAuthInfo) *restConfig { mergedConfig := &restConfig{} // blindly overwrite existing values based on precedence @@ -274,7 +272,7 @@ func getUserIdentificationPartialConfig(configAuthInfo clientcmdAuthInfo) (*rest } // REMOVED: prompting for missing information. - return mergedConfig, nil + return mergedConfig } // ConfirmUsable is a modified copy of k8s.io/kubernetes/pkg/client/unversioned/clientcmd.DirectClientConfig.ConfirmUsable. @@ -672,11 +670,7 @@ func load(data []byte) (*clientcmdConfig, error) { return config, nil } // Note: This does absolutely no kind/version checking or conversions. - data, err := yaml.YAMLToJSON(data) - if err != nil { - return nil, err - } - if err := json.Unmarshal(data, config); err != nil { + if err := yaml.Unmarshal(data, config); err != nil { return nil, err } return config, nil @@ -877,11 +871,11 @@ func newProxierWithNoProxyCIDR(delegate func(req *http.Request) (*url.URL, error noProxyEnv := os.Getenv("NO_PROXY") noProxyRules := strings.Split(noProxyEnv, ",") - cidrs := []*net.IPNet{} + cidrs := []netip.Prefix{} for _, noProxyRule := range noProxyRules { - _, cidr, _ := net.ParseCIDR(noProxyRule) - if cidr != nil { - cidrs = append(cidrs, cidr) + prefix, err := netip.ParsePrefix(noProxyRule) + if err == nil { + cidrs = append(cidrs, prefix) } } @@ -892,7 +886,7 @@ func newProxierWithNoProxyCIDR(delegate func(req *http.Request) (*url.URL, error return func(req *http.Request) (*url.URL, error) { host := req.URL.Host // for some urls, the Host is already the host, not the host:port - if net.ParseIP(host) == nil { + if _, err := netip.ParseAddr(host); err != nil { var err error host, _, err = net.SplitHostPort(req.URL.Host) if err != nil { @@ -900,15 +894,15 @@ func newProxierWithNoProxyCIDR(delegate func(req *http.Request) (*url.URL, error } } - ip := net.ParseIP(host) - if ip == nil { + ip, err := netip.ParseAddr(host) + if err != nil { return delegate(req) } - for _, cidr := range cidrs { - if cidr.Contains(ip) { - return nil, nil - } + if slices.ContainsFunc(cidrs, func(cidr netip.Prefix) bool { + return cidr.Contains(ip) + }) { + return nil, nil } return delegate(req) @@ -936,14 +930,14 @@ func tlsCacheGet(config *restConfig) (http.RoundTripper, error) { Proxy: newProxierWithNoProxyCIDR(http.ProxyFromEnvironment), TLSHandshakeTimeout: 10 * time.Second, TLSClientConfig: tlsConfig, - Dial: (&net.Dialer{ + DialContext: (&net.Dialer{ Timeout: 30 * time.Second, KeepAlive: 30 * time.Second, - }).Dial, + }).DialContext, } // Allow clients to disable http2 if needed. if s := os.Getenv("DISABLE_HTTP2"); len(s) == 0 { - _ = http2.ConfigureTransport(t) + t.ForceAttemptHTTP2 = true } return t, nil } @@ -1057,20 +1051,20 @@ func (c *restConfig) HasCertAuth() bool { // IMPORTANT if you add fields to this struct, please update IsConfigEmpty() type clientcmdConfig struct { // Clusters is a map of referenceable names to cluster configs - Clusters clustersMap `json:"clusters"` + Clusters clustersMap `yaml:"clusters"` // AuthInfos is a map of referenceable names to user configs - AuthInfos authInfosMap `json:"users"` + AuthInfos authInfosMap `yaml:"users"` // Contexts is a map of referenceable names to context configs - Contexts contextsMap `json:"contexts"` + Contexts contextsMap `yaml:"contexts"` // CurrentContext is the name of the context that you would like to use by default - CurrentContext string `json:"current-context"` + CurrentContext string `yaml:"current-context"` } type clustersMap map[string]*clientcmdCluster -func (m *clustersMap) UnmarshalJSON(data []byte) error { +func (m *clustersMap) UnmarshalYAML(value *yaml.Node) error { var a []v1NamedCluster - if err := json.Unmarshal(data, &a); err != nil { + if err := value.Decode(&a); err != nil { return err } for _, e := range a { @@ -1082,9 +1076,9 @@ func (m *clustersMap) UnmarshalJSON(data []byte) error { type authInfosMap map[string]*clientcmdAuthInfo -func (m *authInfosMap) UnmarshalJSON(data []byte) error { +func (m *authInfosMap) UnmarshalYAML(value *yaml.Node) error { var a []v1NamedAuthInfo - if err := json.Unmarshal(data, &a); err != nil { + if err := value.Decode(&a); err != nil { return err } for _, e := range a { @@ -1096,9 +1090,9 @@ func (m *authInfosMap) UnmarshalJSON(data []byte) error { type contextsMap map[string]*clientcmdContext -func (m *contextsMap) UnmarshalJSON(data []byte) error { +func (m *contextsMap) UnmarshalYAML(value *yaml.Node) error { var a []v1NamedContext - if err := json.Unmarshal(data, &a); err != nil { + if err := value.Decode(&a); err != nil { return err } for _, e := range a { @@ -1118,19 +1112,32 @@ func clientcmdNewConfig() *clientcmdConfig { } } +// yamlBinaryAsBase64String is a []byte that can be stored in yaml as a !!str, not a !!binary +type yamlBinaryAsBase64String []byte + +func (bin *yamlBinaryAsBase64String) UnmarshalText(text []byte) error { + res := make([]byte, base64.StdEncoding.DecodedLen(len(text))) + n, err := base64.StdEncoding.Decode(res, text) + if err != nil { + return err + } + *bin = res[:n] + return nil +} + // clientcmdCluster is a modified copy of k8s.io/kubernetes/pkg/client/unversioned/clientcmd/api.Cluster. // Cluster contains information about how to communicate with a kubernetes cluster type clientcmdCluster struct { // LocationOfOrigin indicates where this object came from. It is used for round tripping config post-merge, but never serialized. LocationOfOrigin string // Server is the address of the kubernetes cluster (https://hostname:port). - Server string `json:"server"` + Server string `yaml:"server"` // InsecureSkipTLSVerify skips the validity check for the server's certificate. This will make your HTTPS connections insecure. - InsecureSkipTLSVerify bool `json:"insecure-skip-tls-verify,omitempty"` + InsecureSkipTLSVerify bool `yaml:"insecure-skip-tls-verify,omitempty"` // CertificateAuthority is the path to a cert file for the certificate authority. - CertificateAuthority string `json:"certificate-authority,omitempty"` + CertificateAuthority string `yaml:"certificate-authority,omitempty"` // CertificateAuthorityData contains PEM-encoded certificate authority certificates. Overrides CertificateAuthority - CertificateAuthorityData []byte `json:"certificate-authority-data,omitempty"` + CertificateAuthorityData yamlBinaryAsBase64String `yaml:"certificate-authority-data,omitempty"` } // clientcmdAuthInfo is a modified copy of k8s.io/kubernetes/pkg/client/unversioned/clientcmd/api.AuthInfo. @@ -1139,19 +1146,19 @@ type clientcmdAuthInfo struct { // LocationOfOrigin indicates where this object came from. It is used for round tripping config post-merge, but never serialized. LocationOfOrigin string // ClientCertificate is the path to a client cert file for TLS. - ClientCertificate string `json:"client-certificate,omitempty"` + ClientCertificate string `yaml:"client-certificate,omitempty"` // ClientCertificateData contains PEM-encoded data from a client cert file for TLS. Overrides ClientCertificate - ClientCertificateData []byte `json:"client-certificate-data,omitempty"` + ClientCertificateData yamlBinaryAsBase64String `yaml:"client-certificate-data,omitempty"` // ClientKey is the path to a client key file for TLS. - ClientKey string `json:"client-key,omitempty"` + ClientKey string `yaml:"client-key,omitempty"` // ClientKeyData contains PEM-encoded data from a client key file for TLS. Overrides ClientKey - ClientKeyData []byte `json:"client-key-data,omitempty"` + ClientKeyData yamlBinaryAsBase64String `yaml:"client-key-data,omitempty"` // Token is the bearer token for authentication to the kubernetes cluster. - Token string `json:"token,omitempty"` + Token string `yaml:"token,omitempty"` // Username is the username for basic authentication to the kubernetes cluster. - Username string `json:"username,omitempty"` + Username string `yaml:"username,omitempty"` // Password is the password for basic authentication to the kubernetes cluster. - Password string `json:"password,omitempty"` + Password string `yaml:"password,omitempty"` } // clientcmdContext is a modified copy of k8s.io/kubernetes/pkg/client/unversioned/clientcmd/api.Context. @@ -1160,36 +1167,36 @@ type clientcmdContext struct { // LocationOfOrigin indicates where this object came from. It is used for round tripping config post-merge, but never serialized. LocationOfOrigin string // Cluster is the name of the cluster for this context - Cluster string `json:"cluster"` + Cluster string `yaml:"cluster"` // AuthInfo is the name of the authInfo for this context - AuthInfo string `json:"user"` + AuthInfo string `yaml:"user"` // Namespace is the default namespace to use on unspecified requests - Namespace string `json:"namespace,omitempty"` + Namespace string `yaml:"namespace,omitempty"` } // v1NamedCluster is a modified copy of k8s.io/kubernetes/pkg/client/unversioned/clientcmd/api.v1.NamedCluster. // NamedCluster relates nicknames to cluster information type v1NamedCluster struct { // Name is the nickname for this Cluster - Name string `json:"name"` + Name string `yaml:"name"` // Cluster holds the cluster information - Cluster clientcmdCluster `json:"cluster"` + Cluster clientcmdCluster `yaml:"cluster"` } // v1NamedContext is a modified copy of k8s.io/kubernetes/pkg/client/unversioned/clientcmd/api.v1.NamedContext. // NamedContext relates nicknames to context information type v1NamedContext struct { // Name is the nickname for this Context - Name string `json:"name"` + Name string `yaml:"name"` // Context holds the context information - Context clientcmdContext `json:"context"` + Context clientcmdContext `yaml:"context"` } // v1NamedAuthInfo is a modified copy of k8s.io/kubernetes/pkg/client/unversioned/clientcmd/api.v1.NamedAuthInfo. // NamedAuthInfo relates nicknames to auth information type v1NamedAuthInfo struct { // Name is the nickname for this AuthInfo - Name string `json:"name"` + Name string `yaml:"name"` // AuthInfo holds the auth information - AuthInfo clientcmdAuthInfo `json:"user"` + AuthInfo clientcmdAuthInfo `yaml:"user"` } diff --git a/openshift/openshift-copies_test.go b/openshift/openshift-copies_test.go index 8346db1a41..1fdc37d81a 100644 --- a/openshift/openshift-copies_test.go +++ b/openshift/openshift-copies_test.go @@ -1,14 +1,23 @@ package openshift import ( + "encoding" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" ) const fixtureKubeConfigPath = "testdata/admin.kubeconfig" +var ( + _ yaml.Unmarshaler = (*clustersMap)(nil) + _ yaml.Unmarshaler = (*authInfosMap)(nil) + _ yaml.Unmarshaler = (*contextsMap)(nil) + _ encoding.TextUnmarshaler = (*yamlBinaryAsBase64String)(nil) +) + // These are only smoke tests based on the skopeo integration test cluster. Error handling, non-trivial configuration merging, // and any other situations are not currently covered. diff --git a/openshift/openshift.go b/openshift/openshift.go index b2e4dfd9e8..f3d5662e66 100644 --- a/openshift/openshift.go +++ b/openshift/openshift.go @@ -67,14 +67,14 @@ func newOpenshiftClient(ref openshiftReference) (*openshiftClient, error) { // doRequest performs a correctly authenticated request to a specified path, and returns response body or an error object. func (c *openshiftClient) doRequest(ctx context.Context, method, path string, requestBody []byte) ([]byte, error) { - url := *c.baseURL - url.Path = path + requestURL := *c.baseURL + requestURL.Path = path var requestBodyReader io.Reader if requestBody != nil { logrus.Debugf("Will send body: %s", requestBody) requestBodyReader = bytes.NewReader(requestBody) } - req, err := http.NewRequestWithContext(ctx, method, url.String(), requestBodyReader) + req, err := http.NewRequestWithContext(ctx, method, requestURL.String(), requestBodyReader) if err != nil { return nil, err } @@ -90,7 +90,7 @@ func (c *openshiftClient) doRequest(ctx context.Context, method, path string, re req.Header.Set("Content-Type", "application/json") } - logrus.Debugf("%s %s", method, url.Redacted()) + logrus.Debugf("%s %s", method, requestURL.Redacted()) res, err := c.httpClient.Do(req) if err != nil { return nil, err @@ -146,11 +146,11 @@ func (c *openshiftClient) getImage(ctx context.Context, imageStreamImageName str // convertDockerImageReference takes an image API DockerImageReference value and returns a reference we can actually use; // currently OpenShift stores the cluster-internal service IPs here, which are unusable from the outside. func (c *openshiftClient) convertDockerImageReference(ref string) (string, error) { - parts := strings.SplitN(ref, "/", 2) - if len(parts) != 2 { + _, repo, gotRepo := strings.Cut(ref, "/") + if !gotRepo { return "", fmt.Errorf("Invalid format of docker reference %s: missing '/'", ref) } - return reference.Domain(c.ref.dockerReference) + "/" + parts[1], nil + return reference.Domain(c.ref.dockerReference) + "/" + repo, nil } // These structs are subsets of github.com/openshift/origin/pkg/image/api/v1 and its dependencies. diff --git a/openshift/openshift_dest.go b/openshift/openshift_dest.go index d5dbaf27eb..92aec0266c 100644 --- a/openshift/openshift_dest.go +++ b/openshift/openshift_dest.go @@ -17,10 +17,12 @@ import ( "github.com/containers/image/v5/internal/imagedestination/impl" "github.com/containers/image/v5/internal/imagedestination/stubs" "github.com/containers/image/v5/internal/private" + "github.com/containers/image/v5/internal/set" "github.com/containers/image/v5/internal/signature" "github.com/containers/image/v5/manifest" "github.com/containers/image/v5/types" "github.com/opencontainers/go-digest" + "golang.org/x/exp/slices" ) type openshiftImageDestination struct { @@ -180,12 +182,11 @@ func (d *openshiftImageDestination) PutSignaturesWithFormat(ctx context.Context, if err != nil { return err } - existingSigNames := map[string]struct{}{} + existingSigNames := set.New[string]() for _, sig := range image.Signatures { - existingSigNames[sig.objectMeta.Name] = struct{}{} + existingSigNames.Add(sig.objectMeta.Name) } -sigExists: for _, newSigWithFormat := range signatures { newSigSimple, ok := newSigWithFormat.(signature.SimpleSigning) if !ok { @@ -193,10 +194,10 @@ sigExists: } newSig := newSigSimple.UntrustedSignature() - for _, existingSig := range image.Signatures { - if existingSig.Type == imageSignatureTypeAtomic && bytes.Equal(existingSig.Content, newSig) { - continue sigExists - } + if slices.ContainsFunc(image.Signatures, func(existingSig imageSignature) bool { + return existingSig.Type == imageSignatureTypeAtomic && bytes.Equal(existingSig.Content, newSig) + }) { + continue } // The API expect us to invent a new unique name. This is racy, but hopefully good enough. @@ -208,7 +209,7 @@ sigExists: return fmt.Errorf("generating random signature len %d: %w", n, err) } signatureName = fmt.Sprintf("%s@%032x", imageStreamImageName, randBytes) - if _, ok := existingSigNames[signatureName]; !ok { + if !existingSigNames.Contains(signatureName) { break } } diff --git a/openshift/openshift_transport.go b/openshift/openshift_transport.go index f7971a48f5..0ba435d560 100644 --- a/openshift/openshift_transport.go +++ b/openshift/openshift_transport.go @@ -4,7 +4,6 @@ import ( "context" "errors" "fmt" - "regexp" "strings" "github.com/containers/image/v5/docker/policyconfiguration" @@ -12,6 +11,7 @@ import ( genericImage "github.com/containers/image/v5/internal/image" "github.com/containers/image/v5/transports" "github.com/containers/image/v5/types" + "github.com/containers/storage/pkg/regexp" ) func init() { @@ -35,7 +35,7 @@ func (t openshiftTransport) ParseReference(reference string) (types.ImageReferen // Note that imageNameRegexp is namespace/stream:tag, this // is HOSTNAME/namespace/stream:tag or parent prefixes. // Keep this in sync with imageNameRegexp! -var scopeRegexp = regexp.MustCompile("^[^/]*(/[^:/]*(/[^:/]*(:[^:/]*)?)?)?$") +var scopeRegexp = regexp.Delayed("^[^/]*(/[^:/]*(/[^:/]*(:[^:/]*)?)?)?$") // ValidatePolicyConfigurationScope checks that scope is a valid name for a signature.PolicyTransportScopes keys // (i.e. a valid PolicyConfigurationIdentity() or PolicyConfigurationNamespaces() return value). @@ -89,7 +89,7 @@ func (ref openshiftReference) Transport() types.ImageTransport { // StringWithinTransport returns a string representation of the reference, which MUST be such that // reference.Transport().ParseReference(reference.StringWithinTransport()) returns an equivalent reference. // NOTE: The returned string is not promised to be equal to the original input to ParseReference; -// e.g. default attribute values omitted by the user may be filled in in the return value, or vice versa. +// e.g. default attribute values omitted by the user may be filled in the return value, or vice versa. // WARNING: Do not use the return value in the UI to describe an image, it does not contain the Transport().Name() prefix. func (ref openshiftReference) StringWithinTransport() string { return reference.FamiliarString(ref.dockerReference) diff --git a/ostree/ostree_transport.go b/ostree/ostree_transport.go index 658d4e9035..d83f85b90f 100644 --- a/ostree/ostree_transport.go +++ b/ostree/ostree_transport.go @@ -10,7 +10,6 @@ import ( "fmt" "os" "path/filepath" - "regexp" "strings" "github.com/containers/image/v5/directory/explicitfilepath" @@ -18,6 +17,7 @@ import ( "github.com/containers/image/v5/internal/image" "github.com/containers/image/v5/transports" "github.com/containers/image/v5/types" + "github.com/containers/storage/pkg/regexp" ) const defaultOSTreeRepo = "/ostree/repo" @@ -75,12 +75,11 @@ type ostreeImageCloser struct { func (t ostreeTransport) ParseReference(ref string) (types.ImageReference, error) { var repo = "" - var image = "" - s := strings.SplitN(ref, "@/", 2) - if len(s) == 1 { - image, repo = s[0], defaultOSTreeRepo + image, repoPart, gotRepoPart := strings.Cut(ref, "@/") + if !gotRepoPart { + repo = defaultOSTreeRepo } else { - image, repo = s[0], "/"+s[1] + repo = "/" + repoPart } return NewReference(image, repo) @@ -134,7 +133,7 @@ func (ref ostreeReference) Transport() types.ImageTransport { // StringWithinTransport returns a string representation of the reference, which MUST be such that // reference.Transport().ParseReference(reference.StringWithinTransport()) returns an equivalent reference. // NOTE: The returned string is not promised to be equal to the original input to ParseReference; -// e.g. default attribute values omitted by the user may be filled in in the return value, or vice versa. +// e.g. default attribute values omitted by the user may be filled in the return value, or vice versa. // WARNING: Do not use the return value in the UI to describe an image, it does not contain the Transport().Name() prefix. func (ref ostreeReference) StringWithinTransport() string { return fmt.Sprintf("%s@%s", ref.image, ref.repo) @@ -157,11 +156,11 @@ func (ref ostreeReference) PolicyConfigurationIdentity() string { // It is STRONGLY recommended for the first element, if any, to be a prefix of PolicyConfigurationIdentity(), // and each following element to be a prefix of the element preceding it. func (ref ostreeReference) PolicyConfigurationNamespaces() []string { - s := strings.SplitN(ref.image, ":", 2) - if len(s) != 2 { // Coverage: Should never happen, NewReference above ensures ref.image has a :tag. + repo, _, gotTag := strings.Cut(ref.image, ":") + if !gotTag { // Coverage: Should never happen, NewReference above ensures ref.image has a :tag. panic(fmt.Sprintf("Internal inconsistency: ref.image value %q does not have a :tag", ref.image)) } - name := s[0] + name := repo res := []string{} for { res = append(res, fmt.Sprintf("%s:%s", ref.repo, name)) @@ -216,7 +215,7 @@ func (ref ostreeReference) DeleteImage(ctx context.Context, sys *types.SystemCon return errors.New("Deleting images not implemented for ostree: images") } -var ostreeRefRegexp = regexp.MustCompile(`^[A-Za-z0-9.-]$`) +var ostreeRefRegexp = regexp.Delayed(`^[A-Za-z0-9.-]$`) func encodeOStreeRef(in string) string { var buffer bytes.Buffer diff --git a/pkg/blobcache/blobcache_test.go b/pkg/blobcache/blobcache_test.go index 7f9d0f9ba0..fa20d003b4 100644 --- a/pkg/blobcache/blobcache_test.go +++ b/pkg/blobcache/blobcache_test.go @@ -69,9 +69,9 @@ func makeLayer(filename string, repeat int, compression archive.Compression) ([] func TestBlobCache(t *testing.T) { cacheDir := t.TempDir() - systemContext := types.SystemContext{} + systemContext := types.SystemContext{BlobInfoCacheDir: "/dev/null/this/does/not/exist"} - for _, repeat := range []int{1, 10, 100, 1000, 10000} { + for _, repeat := range []int{1, 10000} { for _, desiredCompression := range []types.LayerCompression{types.PreserveOriginal, types.Compress, types.Decompress} { for _, layerCompression := range []archive.Compression{archive.Uncompressed, archive.Gzip} { // Create a layer with the specified layerCompression. diff --git a/pkg/blobcache/src.go b/pkg/blobcache/src.go index 60677470fc..e4908ed4f4 100644 --- a/pkg/blobcache/src.go +++ b/pkg/blobcache/src.go @@ -10,9 +10,9 @@ import ( "github.com/containers/image/v5/internal/image" "github.com/containers/image/v5/internal/imagesource" "github.com/containers/image/v5/internal/imagesource/impl" + "github.com/containers/image/v5/internal/manifest" "github.com/containers/image/v5/internal/private" "github.com/containers/image/v5/internal/signature" - "github.com/containers/image/v5/manifest" "github.com/containers/image/v5/pkg/compression" "github.com/containers/image/v5/transports" "github.com/containers/image/v5/types" @@ -138,7 +138,7 @@ func (s *blobCacheSource) LayerInfosForCopy(ctx context.Context, instanceDigest var replaceDigest []byte var err error blobFile := s.reference.blobPath(info.Digest, false) - alternate := "" + var alternate string switch s.reference.compress { case types.Compress: alternate = blobFile + compressedNote @@ -200,14 +200,14 @@ func streamChunksFromFile(streams chan io.ReadCloser, errs chan error, file io.R defer file.Close() for _, c := range chunks { - // Always seek to the desired offest; that way we don’t need to care about the consumer + // Always seek to the desired offset; that way we don’t need to care about the consumer // not reading all of the chunk, or about the position going backwards. if _, err := file.Seek(int64(c.Offset), io.SeekStart); err != nil { errs <- err break } s := signalCloseReader{ - closed: make(chan interface{}), + closed: make(chan struct{}), stream: io.LimitReader(file, int64(c.Length)), } streams <- s @@ -218,7 +218,7 @@ func streamChunksFromFile(streams chan io.ReadCloser, errs chan error, file io.R } type signalCloseReader struct { - closed chan interface{} + closed chan struct{} stream io.Reader } diff --git a/pkg/blobinfocache/internal/prioritize/prioritize_test.go b/pkg/blobinfocache/internal/prioritize/prioritize_test.go index 39ca7b9125..77167e904c 100644 --- a/pkg/blobinfocache/internal/prioritize/prioritize_test.go +++ b/pkg/blobinfocache/internal/prioritize/prioritize_test.go @@ -10,6 +10,7 @@ import ( "github.com/containers/image/v5/types" "github.com/opencontainers/go-digest" "github.com/stretchr/testify/assert" + "golang.org/x/exp/slices" ) const ( @@ -139,7 +140,7 @@ func TestCandidateSortStateLess(t *testing.T) { func TestCandidateSortStateSwap(t *testing.T) { freshCSS := func() candidateSortState { // Return a deep copy of cssLiteral which is safe to modify. res := cssLiteral - res.cs = append([]CandidateWithTime{}, cssLiteral.cs...) + res.cs = slices.Clone(cssLiteral.cs) return res } @@ -157,7 +158,7 @@ func TestCandidateSortStateSwap(t *testing.T) { func TestDestructivelyPrioritizeReplacementCandidatesWithMax(t *testing.T) { for _, max := range []int{0, 1, replacementAttempts, 100} { // Just a smoke test; we mostly rely on test coverage in TestCandidateSortStateLess - res := destructivelyPrioritizeReplacementCandidatesWithMax(append([]CandidateWithTime{}, cssLiteral.cs...), digestCompressedPrimary, digestUncompressed, max) + res := destructivelyPrioritizeReplacementCandidatesWithMax(slices.Clone(cssLiteral.cs), digestCompressedPrimary, digestUncompressed, max) if max > len(cssExpectedReplacementCandidates) { max = len(cssExpectedReplacementCandidates) } @@ -167,6 +168,6 @@ func TestDestructivelyPrioritizeReplacementCandidatesWithMax(t *testing.T) { func TestDestructivelyPrioritizeReplacementCandidates(t *testing.T) { // Just a smoke test; we mostly rely on test coverage in TestCandidateSortStateLess - res := DestructivelyPrioritizeReplacementCandidates(append([]CandidateWithTime{}, cssLiteral.cs...), digestCompressedPrimary, digestUncompressed) + res := DestructivelyPrioritizeReplacementCandidates(slices.Clone(cssLiteral.cs), digestCompressedPrimary, digestUncompressed) assert.Equal(t, cssExpectedReplacementCandidates[:replacementAttempts], res) } diff --git a/pkg/blobinfocache/memory/memory.go b/pkg/blobinfocache/memory/memory.go index 426640366f..427610fab0 100644 --- a/pkg/blobinfocache/memory/memory.go +++ b/pkg/blobinfocache/memory/memory.go @@ -6,6 +6,7 @@ import ( "time" "github.com/containers/image/v5/internal/blobinfocache" + "github.com/containers/image/v5/internal/set" "github.com/containers/image/v5/pkg/blobinfocache/internal/prioritize" "github.com/containers/image/v5/types" digest "github.com/opencontainers/go-digest" @@ -19,12 +20,12 @@ type locationKey struct { blobDigest digest.Digest } -// cache implements an in-memory-only BlobInfoCache +// cache implements an in-memory-only BlobInfoCache. type cache struct { mutex sync.Mutex // The following fields can only be accessed with mutex held. uncompressedDigests map[digest.Digest]digest.Digest - digestsByUncompressed map[digest.Digest]map[digest.Digest]struct{} // stores a set of digests for each uncompressed digest + digestsByUncompressed map[digest.Digest]*set.Set[digest.Digest] // stores a set of digests for each uncompressed digest knownLocations map[locationKey]map[types.BICLocationReference]time.Time // stores last known existence time for each location reference compressors map[digest.Digest]string // stores a compressor name, or blobinfocache.Unknown, for each digest } @@ -44,7 +45,7 @@ func New() types.BlobInfoCache { func new2() *cache { return &cache{ uncompressedDigests: map[digest.Digest]digest.Digest{}, - digestsByUncompressed: map[digest.Digest]map[digest.Digest]struct{}{}, + digestsByUncompressed: map[digest.Digest]*set.Set[digest.Digest]{}, knownLocations: map[locationKey]map[types.BICLocationReference]time.Time{}, compressors: map[digest.Digest]string{}, } @@ -67,7 +68,7 @@ func (mem *cache) uncompressedDigestLocked(anyDigest digest.Digest) digest.Diges // Presence in digestsByUncompressed implies that anyDigest must already refer to an uncompressed digest. // This way we don't have to waste storage space with trivial (uncompressed, uncompressed) mappings // when we already record a (compressed, uncompressed) pair. - if m, ok := mem.digestsByUncompressed[anyDigest]; ok && len(m) > 0 { + if s, ok := mem.digestsByUncompressed[anyDigest]; ok && !s.Empty() { return anyDigest } return "" @@ -88,10 +89,10 @@ func (mem *cache) RecordDigestUncompressedPair(anyDigest digest.Digest, uncompre anyDigestSet, ok := mem.digestsByUncompressed[uncompressed] if !ok { - anyDigestSet = map[digest.Digest]struct{}{} + anyDigestSet = set.New[digest.Digest]() mem.digestsByUncompressed[uncompressed] = anyDigestSet } - anyDigestSet[anyDigest] = struct{}{} // Possibly writing the same struct{}{} presence marker again. + anyDigestSet.Add(anyDigest) } // RecordKnownLocation records that a blob with the specified digest exists within the specified (transport, scope) scope, @@ -171,10 +172,11 @@ func (mem *cache) candidateLocations(transport types.ImageTransport, scope types var uncompressedDigest digest.Digest // = "" if canSubstitute { if uncompressedDigest = mem.uncompressedDigestLocked(primaryDigest); uncompressedDigest != "" { - otherDigests := mem.digestsByUncompressed[uncompressedDigest] // nil if not present in the map - for d := range otherDigests { - if d != primaryDigest && d != uncompressedDigest { - res = mem.appendReplacementCandidates(res, transport, scope, d, requireCompressionInfo) + if otherDigests, ok := mem.digestsByUncompressed[uncompressedDigest]; ok { + for _, d := range otherDigests.Values() { + if d != primaryDigest && d != uncompressedDigest { + res = mem.appendReplacementCandidates(res, transport, scope, d, requireCompressionInfo) + } } } if uncompressedDigest != primaryDigest { diff --git a/pkg/cli/sigstore/params/sigstore.go b/pkg/cli/sigstore/params/sigstore.go new file mode 100644 index 0000000000..0151b9acb0 --- /dev/null +++ b/pkg/cli/sigstore/params/sigstore.go @@ -0,0 +1,75 @@ +package params + +import ( + "bytes" + "fmt" + "os" + + "gopkg.in/yaml.v3" +) + +// SigningParameterFile collects parameters used for creating sigstore signatures. +// +// To consume such a file, most callers should use c/image/pkg/cli/sigstore instead +// of dealing with this type explicitly using ParseFile. +// +// This type is exported primarily to allow creating parameter files programmatically +// (and eventually this subpackage should provide an API to convert this type into +// the appropriate file contents, so that callers don’t need to do that manually). +type SigningParameterFile struct { + // Keep this in sync with docs/containers-sigstore-signing-params.yaml.5.md ! + + PrivateKeyFile string `yaml:"privateKeyFile,omitempty"` // If set, sign using a private key stored in this file. + PrivateKeyPassphraseFile string `yaml:"privateKeyPassphraseFile,omitempty"` // A file that contains the passprase required for PrivateKeyFile. + + Fulcio *SigningParameterFileFulcio `yaml:"fulcio,omitempty"` // If set, sign using a short-lived key and a Fulcio-issued certificate. + + RekorURL string `yaml:"rekorURL,omitempty"` // If set, upload the signature to the specified Rekor server, and include a log inclusion proof in the signature. +} + +// SigningParameterFileFulcio is a subset of SigningParameterFile dedicated to Fulcio parameters. +type SigningParameterFileFulcio struct { + // Keep this in sync with docs/containers-sigstore-signing-params.yaml.5.md ! + + FulcioURL string `yaml:"fulcioURL,omitempty"` // URL of the Fulcio server. Required. + + // How to obtain the OIDC ID token required by Fulcio. Required. + OIDCMode OIDCMode `yaml:"oidcMode,omitempty"` + + // oidcMode = staticToken + OIDCIDToken string `yaml:"oidcIDToken,omitempty"` + + // oidcMode = deviceGrant || interactive + OIDCIssuerURL string `yaml:"oidcIssuerURL,omitempty"` // + OIDCClientID string `yaml:"oidcClientID,omitempty"` + OIDCClientSecret string `yaml:"oidcClientSecret,omitempty"` +} + +type OIDCMode string + +const ( + // OIDCModeStaticToken means the parameter file contains an user-provided OIDC ID token value. + OIDCModeStaticToken OIDCMode = "staticToken" + // OIDCModeDeviceGrant specifies the OIDC ID token should be obtained using a device authorization grant (RFC 8628). + OIDCModeDeviceGrant OIDCMode = "deviceGrant" + // OIDCModeInteractive specifies the OIDC ID token should be obtained interactively (automatically opening a browser, + // or interactively prompting the user.) + OIDCModeInteractive OIDCMode = "interactive" +) + +// ParseFile parses a SigningParameterFile at the specified path. +// +// Most consumers of the parameter file should use c/image/pkg/cli/sigstore to obtain a *signer.Signer instead. +func ParseFile(path string) (*SigningParameterFile, error) { + var res SigningParameterFile + source, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("reading %q: %w", path, err) + } + dec := yaml.NewDecoder(bytes.NewReader(source)) + dec.KnownFields(true) + if err = dec.Decode(&res); err != nil { + return nil, fmt.Errorf("parsing %q: %w", path, err) + } + return &res, nil +} diff --git a/pkg/cli/sigstore/sigstore.go b/pkg/cli/sigstore/sigstore.go new file mode 100644 index 0000000000..62520c212f --- /dev/null +++ b/pkg/cli/sigstore/sigstore.go @@ -0,0 +1,117 @@ +package sigstore + +import ( + "errors" + "fmt" + "io" + "net/url" + + "github.com/containers/image/v5/pkg/cli" + "github.com/containers/image/v5/pkg/cli/sigstore/params" + "github.com/containers/image/v5/signature/signer" + "github.com/containers/image/v5/signature/sigstore" + "github.com/containers/image/v5/signature/sigstore/fulcio" + "github.com/containers/image/v5/signature/sigstore/rekor" +) + +// Options collects data that the caller should provide to NewSignerFromParameterFile. +// The caller should set all fields unless documented otherwise. +type Options struct { + PrivateKeyPassphrasePrompt func(keyFile string) (string, error) // A function to call to interactively prompt for a passphrase + Stdin io.Reader + Stdout io.Writer +} + +// NewSignerFromParameterFile returns a signature.Signer which creates sigstore signatures based a parameter file at the specified path. +// +// The caller must call Close() on the returned Signer. +func NewSignerFromParameterFile(path string, options *Options) (*signer.Signer, error) { + params, err := params.ParseFile(path) + if err != nil { + return nil, fmt.Errorf("setting up signing using parameter file %q: %w", path, err) + } + return newSignerFromParameterData(params, options) +} + +// newSignerFromParameterData returns a signature.Signer which creates sigstore signatures based on parameter file contents. +// +// The caller must call Close() on the returned Signer. +func newSignerFromParameterData(params *params.SigningParameterFile, options *Options) (*signer.Signer, error) { + opts := []sigstore.Option{} + if params.PrivateKeyFile != "" { + var getPassphrase func(keyFile string) (string, error) + switch { + case params.PrivateKeyPassphraseFile != "": + getPassphrase = func(_ string) (string, error) { + return cli.ReadPassphraseFile(params.PrivateKeyPassphraseFile) + } + case options.PrivateKeyPassphrasePrompt != nil: + getPassphrase = options.PrivateKeyPassphrasePrompt + default: // This shouldn’t happen, the caller is expected to set options.PrivateKeyPassphrasePrompt + return nil, fmt.Errorf("private key %s specified, but no way to get a passphrase", params.PrivateKeyFile) + } + passphrase, err := getPassphrase(params.PrivateKeyFile) + if err != nil { + return nil, err + } + opts = append(opts, sigstore.WithPrivateKeyFile(params.PrivateKeyFile, []byte(passphrase))) + } + + if params.Fulcio != nil { + fulcioOpt, err := fulcioOption(params.Fulcio, options) + if err != nil { + return nil, err + } + opts = append(opts, fulcioOpt) + } + + if params.RekorURL != "" { + rekorURL, err := url.Parse(params.RekorURL) + if err != nil { + return nil, fmt.Errorf("parsing rekorURL %q: %w", params.RekorURL, err) + } + opts = append(opts, rekor.WithRekor(rekorURL)) + } + + return sigstore.NewSigner(opts...) +} + +// fulcioOption returns a sigstore.Option for Fulcio use based on f. +func fulcioOption(f *params.SigningParameterFileFulcio, options *Options) (sigstore.Option, error) { + if f.FulcioURL == "" { + return nil, errors.New("missing fulcioURL") + } + fulcioURL, err := url.Parse(f.FulcioURL) + if err != nil { + return nil, fmt.Errorf("parsing fulcioURL %q: %w", f.FulcioURL, err) + } + + if f.OIDCMode == params.OIDCModeStaticToken { + if f.OIDCIDToken == "" { + return nil, errors.New("missing oidcToken") + } + return fulcio.WithFulcioAndPreexistingOIDCIDToken(fulcioURL, f.OIDCIDToken), nil + } + + if f.OIDCIssuerURL == "" { + return nil, errors.New("missing oidcIssuerURL") + } + oidcIssuerURL, err := url.Parse(f.OIDCIssuerURL) + if err != nil { + return nil, fmt.Errorf("parsing oidcIssuerURL %q: %w", f.OIDCIssuerURL, err) + } + switch f.OIDCMode { + case params.OIDCModeDeviceGrant: + return fulcio.WithFulcioAndDeviceAuthorizationGrantOIDC(fulcioURL, oidcIssuerURL, f.OIDCClientID, f.OIDCClientSecret, + options.Stdout), nil + case params.OIDCModeInteractive: + return fulcio.WithFulcioAndInteractiveOIDC(fulcioURL, oidcIssuerURL, f.OIDCClientID, f.OIDCClientSecret, + options.Stdin, options.Stdout), nil + case "": + return nil, errors.New("missing oidcMode") + case params.OIDCModeStaticToken: + return nil, errors.New("internal inconsistency: SigningParameterFileOIDCModeStaticToken was supposed to already be handled") + default: + return nil, fmt.Errorf("unknown oidcMode value %q", f.OIDCMode) + } +} diff --git a/pkg/compression/compression.go b/pkg/compression/compression.go index ce688d1170..4443dda7ff 100644 --- a/pkg/compression/compression.go +++ b/pkg/compression/compression.go @@ -30,7 +30,7 @@ var ( // Zstd compression. Zstd = internal.NewAlgorithm(types.ZstdAlgorithmName, types.ZstdAlgorithmName, []byte{0x28, 0xb5, 0x2f, 0xfd}, ZstdDecompressor, zstdCompressor) - // Zstd:chunked compression. + // ZstdChunked is a Zstd compression with chunk metadta which allows random access to individual files. ZstdChunked = internal.NewAlgorithm(types.ZstdChunkedAlgorithmName, types.ZstdAlgorithmName, /* Note: InternalUnstableUndocumentedMIMEQuestionMark is not ZstdChunkedAlgorithmName */ nil, ZstdDecompressor, compressor.ZstdCompressor) diff --git a/pkg/docker/config/config.go b/pkg/docker/config/config.go index 0790a47f24..0e3003cecb 100644 --- a/pkg/docker/config/config.go +++ b/pkg/docker/config/config.go @@ -12,6 +12,7 @@ import ( "strings" "github.com/containers/image/v5/docker/reference" + "github.com/containers/image/v5/internal/set" "github.com/containers/image/v5/pkg/sysregistriesv2" "github.com/containers/image/v5/types" "github.com/containers/storage/pkg/homedir" @@ -32,11 +33,6 @@ type dockerConfigFile struct { CredHelpers map[string]string `json:"credHelpers,omitempty"` } -type authPath struct { - path string - legacyFormat bool -} - var ( defaultPerUIDPathFormat = filepath.FromSlash("/run/containers/%d/auth.json") xdgConfigHomePath = filepath.FromSlash("containers/auth.json") @@ -52,11 +48,24 @@ var ( ErrNotSupported = errors.New("not supported") ) +// authPath combines a path to a file with container registry access keys, +// along with expected properties of that path (currently just whether it's) +// legacy format or not. +type authPath struct { + path string + legacyFormat bool +} + +// newAuthPathDefault constructs an authPath in non-legacy format. +func newAuthPathDefault(path string) authPath { + return authPath{path: path, legacyFormat: false} +} + // SetCredentials stores the username and password in a location // appropriate for sys and the users’ configuration. // A valid key is a repository, a namespace within a registry, or a registry hostname; // using forms other than just a registry may fail depending on configuration. -// Returns a human-redable description of the location that was updated. +// Returns a human-readable description of the location that was updated. // NOTE: The return value is only intended to be read by humans; its form is not an API, // it may change (or new forms can be added) any time. func SetCredentials(sys *types.SystemContext, key, username, password string) (string, error) { @@ -78,25 +87,28 @@ func SetCredentials(sys *types.SystemContext, key, username, password string) (s switch helper { // Special-case the built-in helpers for auth files. case sysregistriesv2.AuthenticationFileHelper: - desc, err = modifyJSON(sys, func(auths *dockerConfigFile) (bool, error) { + desc, err = modifyJSON(sys, func(auths *dockerConfigFile) (bool, string, error) { if ch, exists := auths.CredHelpers[key]; exists { if isNamespaced { - return false, unsupportedNamespaceErr(ch) + return false, "", unsupportedNamespaceErr(ch) + } + desc, err := setAuthToCredHelper(ch, key, username, password) + if err != nil { + return false, "", err } - return false, setAuthToCredHelper(ch, key, username, password) + return false, desc, nil } creds := base64.StdEncoding.EncodeToString([]byte(username + ":" + password)) newCreds := dockerAuthConfig{Auth: creds} auths.AuthConfigs[key] = newCreds - return true, nil + return true, "", nil }) // External helpers. default: if isNamespaced { err = unsupportedNamespaceErr(helper) } else { - desc = fmt.Sprintf("credential helper: %s", helper) - err = setAuthToCredHelper(helper, key, username, password) + desc, err = setAuthToCredHelper(helper, key, username, password) } } if err != nil { @@ -128,10 +140,7 @@ func GetAllCredentials(sys *types.SystemContext) (map[string]types.DockerAuthCon // possible sources, and then call `GetCredentials` on them. That // prevents us from having to reverse engineer the logic in // `GetCredentials`. - allKeys := make(map[string]bool) - addKey := func(s string) { - allKeys[s] = true - } + allKeys := set.New[string]() // To use GetCredentials, we must at least convert the URL forms into host names. // While we're at it, we’ll also canonicalize docker.io to the standard format. @@ -146,8 +155,8 @@ func GetAllCredentials(sys *types.SystemContext) (map[string]types.DockerAuthCon // Special-case the built-in helper for auth files. case sysregistriesv2.AuthenticationFileHelper: for _, path := range getAuthFilePaths(sys, homedir.Get()) { - // readJSONFile returns an empty map in case the path doesn't exist. - auths, err := readJSONFile(path.path, path.legacyFormat) + // parse returns an empty map in case the path doesn't exist. + auths, err := path.parse() if err != nil { return nil, fmt.Errorf("reading JSON file %q: %w", path.path, err) } @@ -155,14 +164,14 @@ func GetAllCredentials(sys *types.SystemContext) (map[string]types.DockerAuthCon // direct mapping to a registry, so we can just // walk the map. for registry := range auths.CredHelpers { - addKey(registry) + allKeys.Add(registry) } for key := range auths.AuthConfigs { key := normalizeAuthFileKey(key, path.legacyFormat) if key == normalizedDockerIORegistry { key = "docker.io" } - addKey(key) + allKeys.Add(key) } } // External helpers. @@ -177,7 +186,7 @@ func GetAllCredentials(sys *types.SystemContext) (map[string]types.DockerAuthCon } } for registry := range creds { - addKey(registry) + allKeys.Add(registry) } } } @@ -185,7 +194,7 @@ func GetAllCredentials(sys *types.SystemContext) (map[string]types.DockerAuthCon // Now use `GetCredentials` to the specific auth configs for each // previously listed registry. authConfigs := make(map[string]types.DockerAuthConfig) - for key := range allKeys { + for _, key := range allKeys.Values() { authConf, err := GetCredentials(sys, key) if err != nil { // Note: we rely on the logging in `GetCredentials`. @@ -205,32 +214,32 @@ func GetAllCredentials(sys *types.SystemContext) (map[string]types.DockerAuthCon // by tests. func getAuthFilePaths(sys *types.SystemContext, homeDir string) []authPath { paths := []authPath{} - pathToAuth, lf, err := getPathToAuth(sys) + pathToAuth, userSpecifiedPath, err := getPathToAuth(sys) if err == nil { - paths = append(paths, authPath{path: pathToAuth, legacyFormat: lf}) + paths = append(paths, pathToAuth) } else { // Error means that the path set for XDG_RUNTIME_DIR does not exist // but we don't want to completely fail in the case that the user is pulling a public image // Logging the error as a warning instead and moving on to pulling the image logrus.Warnf("%v: Trying to pull image in the event that it is a public image.", err) } - xdgCfgHome := os.Getenv("XDG_CONFIG_HOME") - if xdgCfgHome == "" { - xdgCfgHome = filepath.Join(homeDir, ".config") - } - paths = append(paths, authPath{path: filepath.Join(xdgCfgHome, xdgConfigHomePath), legacyFormat: false}) - if dockerConfig := os.Getenv("DOCKER_CONFIG"); dockerConfig != "" { - paths = append(paths, - authPath{path: filepath.Join(dockerConfig, "config.json"), legacyFormat: false}, - ) - } else { + if !userSpecifiedPath { + xdgCfgHome := os.Getenv("XDG_CONFIG_HOME") + if xdgCfgHome == "" { + xdgCfgHome = filepath.Join(homeDir, ".config") + } + paths = append(paths, newAuthPathDefault(filepath.Join(xdgCfgHome, xdgConfigHomePath))) + if dockerConfig := os.Getenv("DOCKER_CONFIG"); dockerConfig != "" { + paths = append(paths, newAuthPathDefault(filepath.Join(dockerConfig, "config.json"))) + } else { + paths = append(paths, + newAuthPathDefault(filepath.Join(homeDir, dockerHomePath)), + ) + } paths = append(paths, - authPath{path: filepath.Join(homeDir, dockerHomePath), legacyFormat: false}, + authPath{path: filepath.Join(homeDir, dockerLegacyHomePath), legacyFormat: true}, ) } - paths = append(paths, - authPath{path: filepath.Join(homeDir, dockerLegacyHomePath), legacyFormat: true}, - ) return paths } @@ -276,7 +285,7 @@ func getCredentialsWithHomeDir(sys *types.SystemContext, key, homeDir string) (t // Anonymous function to query credentials from auth files. getCredentialsFromAuthFiles := func() (types.DockerAuthConfig, string, error) { for _, path := range getAuthFilePaths(sys, homeDir) { - authConfig, err := findCredentialsInFile(key, registry, path.path, path.legacyFormat) + authConfig, err := findCredentialsInFile(key, registry, path) if err != nil { return types.DockerAuthConfig{}, "", err } @@ -383,17 +392,16 @@ func RemoveAuthentication(sys *types.SystemContext, key string) error { if isNamespaced { logrus.Debugf("Not removing credentials because namespaced keys are not supported for the credential helper: %s", helper) return - } else { - err := deleteAuthFromCredHelper(helper, key) - if err == nil { - logrus.Debugf("Credentials for %q were deleted from credential helper %s", key, helper) - isLoggedIn = true - return - } - if credentials.IsErrCredentialsNotFoundMessage(err.Error()) { - logrus.Debugf("Not logged in to %s with credential helper %s", key, helper) - return - } + } + err := deleteAuthFromCredHelper(helper, key) + if err == nil { + logrus.Debugf("Credentials for %q were deleted from credential helper %s", key, helper) + isLoggedIn = true + return + } + if credentials.IsErrCredentialsNotFoundMessage(err.Error()) { + logrus.Debugf("Not logged in to %s with credential helper %s", key, helper) + return } multiErr = multierror.Append(multiErr, fmt.Errorf("removing credentials for %s from credential helper %s: %w", key, helper, err)) } @@ -403,7 +411,7 @@ func RemoveAuthentication(sys *types.SystemContext, key string) error { switch helper { // Special-case the built-in helper for auth files. case sysregistriesv2.AuthenticationFileHelper: - _, err = modifyJSON(sys, func(auths *dockerConfigFile) (bool, error) { + _, err = modifyJSON(sys, func(auths *dockerConfigFile) (bool, string, error) { if innerHelper, exists := auths.CredHelpers[key]; exists { removeFromCredHelper(innerHelper) } @@ -411,7 +419,7 @@ func RemoveAuthentication(sys *types.SystemContext, key string) error { isLoggedIn = true delete(auths.AuthConfigs, key) } - return true, multiErr + return true, "", multiErr }) if err != nil { multiErr = multierror.Append(multiErr, err) @@ -446,18 +454,18 @@ func RemoveAllAuthentication(sys *types.SystemContext) error { switch helper { // Special-case the built-in helper for auth files. case sysregistriesv2.AuthenticationFileHelper: - _, err = modifyJSON(sys, func(auths *dockerConfigFile) (bool, error) { + _, err = modifyJSON(sys, func(auths *dockerConfigFile) (bool, string, error) { for registry, helper := range auths.CredHelpers { // Helpers in auth files are expected // to exist, so no special treatment // for them. if err := deleteAuthFromCredHelper(helper, registry); err != nil { - return false, err + return false, "", err } } auths.CredHelpers = make(map[string]string) auths.AuthConfigs = make(map[string]dockerAuthConfig) - return true, nil + return true, "", nil }) // External helpers. default: @@ -495,28 +503,28 @@ func listAuthsFromCredHelper(credHelper string) (map[string]string, error) { return helperclient.List(p) } -// getPathToAuth gets the path of the auth.json file used for reading and writing credentials -// returns the path, and a bool specifies whether the file is in legacy format -func getPathToAuth(sys *types.SystemContext) (string, bool, error) { +// getPathToAuth gets the path of the auth.json file used for reading and writing credentials, +// and a boolean indicating whether the return value came from an explicit user choice (i.e. not defaults) +func getPathToAuth(sys *types.SystemContext) (authPath, bool, error) { return getPathToAuthWithOS(sys, runtime.GOOS) } // getPathToAuthWithOS is an internal implementation detail of getPathToAuth, // it exists only to allow testing it with an artificial runtime.GOOS. -func getPathToAuthWithOS(sys *types.SystemContext, goOS string) (string, bool, error) { +func getPathToAuthWithOS(sys *types.SystemContext, goOS string) (authPath, bool, error) { if sys != nil { if sys.AuthFilePath != "" { - return sys.AuthFilePath, false, nil + return newAuthPathDefault(sys.AuthFilePath), true, nil } if sys.LegacyFormatAuthFilePath != "" { - return sys.LegacyFormatAuthFilePath, true, nil + return authPath{path: sys.LegacyFormatAuthFilePath, legacyFormat: true}, true, nil } if sys.RootForImplicitAbsolutePaths != "" { - return filepath.Join(sys.RootForImplicitAbsolutePaths, fmt.Sprintf(defaultPerUIDPathFormat, os.Getuid())), false, nil + return newAuthPathDefault(filepath.Join(sys.RootForImplicitAbsolutePaths, fmt.Sprintf(defaultPerUIDPathFormat, os.Getuid()))), false, nil } } if goOS == "windows" || goOS == "darwin" { - return filepath.Join(homedir.Get(), nonLinuxAuthFilePath), false, nil + return newAuthPathDefault(filepath.Join(homedir.Get(), nonLinuxAuthFilePath)), false, nil } runtimeDir := os.Getenv("XDG_RUNTIME_DIR") @@ -528,20 +536,20 @@ func getPathToAuthWithOS(sys *types.SystemContext, goOS string) (string, bool, e // This means the user set the XDG_RUNTIME_DIR variable and either forgot to create the directory // or made a typo while setting the environment variable, // so return an error referring to $XDG_RUNTIME_DIR instead of xdgRuntimeDirPath inside. - return "", false, fmt.Errorf("%q directory set by $XDG_RUNTIME_DIR does not exist. Either create the directory or unset $XDG_RUNTIME_DIR.: %w", runtimeDir, err) + return authPath{}, false, fmt.Errorf("%q directory set by $XDG_RUNTIME_DIR does not exist. Either create the directory or unset $XDG_RUNTIME_DIR.: %w", runtimeDir, err) } // else ignore err and let the caller fail accessing xdgRuntimeDirPath. - return filepath.Join(runtimeDir, xdgRuntimeDirPath), false, nil + return newAuthPathDefault(filepath.Join(runtimeDir, xdgRuntimeDirPath)), false, nil } - return fmt.Sprintf(defaultPerUIDPathFormat, os.Getuid()), false, nil + return newAuthPathDefault(fmt.Sprintf(defaultPerUIDPathFormat, os.Getuid())), false, nil } -// readJSONFile unmarshals the authentications stored in the auth.json file and returns it +// parse unmarshals the authentications stored in the auth.json file and returns it // or returns an empty dockerConfigFile data structure if auth.json does not exist -// if the file exists and is empty, readJSONFile returns an error -func readJSONFile(path string, legacyFormat bool) (dockerConfigFile, error) { +// if the file exists and is empty, this function returns an error. +func (path authPath) parse() (dockerConfigFile, error) { var auths dockerConfigFile - raw, err := os.ReadFile(path) + raw, err := os.ReadFile(path.path) if err != nil { if os.IsNotExist(err) { auths.AuthConfigs = map[string]dockerAuthConfig{} @@ -550,15 +558,15 @@ func readJSONFile(path string, legacyFormat bool) (dockerConfigFile, error) { return dockerConfigFile{}, err } - if legacyFormat { + if path.legacyFormat { if err = json.Unmarshal(raw, &auths.AuthConfigs); err != nil { - return dockerConfigFile{}, fmt.Errorf("unmarshaling JSON at %q: %w", path, err) + return dockerConfigFile{}, fmt.Errorf("unmarshaling JSON at %q: %w", path.path, err) } return auths, nil } if err = json.Unmarshal(raw, &auths); err != nil { - return dockerConfigFile{}, fmt.Errorf("unmarshaling JSON at %q: %w", path, err) + return dockerConfigFile{}, fmt.Errorf("unmarshaling JSON at %q: %w", path.path, err) } if auths.AuthConfigs == nil { @@ -573,42 +581,48 @@ func readJSONFile(path string, legacyFormat bool) (dockerConfigFile, error) { // modifyJSON finds an auth.json file, calls editor on the contents, and // writes it back if editor returns true. -// Returns a human-redable description of the file, to be returned by SetCredentials. -func modifyJSON(sys *types.SystemContext, editor func(auths *dockerConfigFile) (bool, error)) (string, error) { - path, legacyFormat, err := getPathToAuth(sys) +// Returns a human-readable description of the file, to be returned by SetCredentials. +// +// The editor may also return a human-readable description of the updated location; if it is "", +// the file itself is used. +func modifyJSON(sys *types.SystemContext, editor func(auths *dockerConfigFile) (bool, string, error)) (string, error) { + path, _, err := getPathToAuth(sys) if err != nil { return "", err } - if legacyFormat { - return "", fmt.Errorf("writes to %s using legacy format are not supported", path) + if path.legacyFormat { + return "", fmt.Errorf("writes to %s using legacy format are not supported", path.path) } - dir := filepath.Dir(path) + dir := filepath.Dir(path.path) if err = os.MkdirAll(dir, 0700); err != nil { return "", err } - auths, err := readJSONFile(path, false) + auths, err := path.parse() if err != nil { - return "", fmt.Errorf("reading JSON file %q: %w", path, err) + return "", fmt.Errorf("reading JSON file %q: %w", path.path, err) } - updated, err := editor(&auths) + updated, description, err := editor(&auths) if err != nil { - return "", fmt.Errorf("updating %q: %w", path, err) + return "", fmt.Errorf("updating %q: %w", path.path, err) } if updated { newData, err := json.MarshalIndent(auths, "", "\t") if err != nil { - return "", fmt.Errorf("marshaling JSON %q: %w", path, err) + return "", fmt.Errorf("marshaling JSON %q: %w", path.path, err) } - if err = ioutils.AtomicWriteFile(path, newData, 0600); err != nil { - return "", fmt.Errorf("writing to file %q: %w", path, err) + if err = ioutils.AtomicWriteFile(path.path, newData, 0600); err != nil { + return "", fmt.Errorf("writing to file %q: %w", path.path, err) } } - return path, nil + if description == "" { + description = path.path + } + return description, nil } func getAuthFromCredHelper(credHelper, registry string) (types.DockerAuthConfig, error) { @@ -636,7 +650,9 @@ func getAuthFromCredHelper(credHelper, registry string) (types.DockerAuthConfig, } } -func setAuthToCredHelper(credHelper, registry, username, password string) error { +// setAuthToCredHelper stores (username, password) for registry in credHelper. +// Returns a human-readable description of the destination, to be returned by SetCredentials. +func setAuthToCredHelper(credHelper, registry, username, password string) (string, error) { helperName := fmt.Sprintf("docker-credential-%s", credHelper) p := helperclient.NewShellProgramFunc(helperName) creds := &credentials.Credentials{ @@ -644,7 +660,10 @@ func setAuthToCredHelper(credHelper, registry, username, password string) error Username: username, Secret: password, } - return helperclient.Store(p, creds) + if err := helperclient.Store(p, creds); err != nil { + return "", err + } + return fmt.Sprintf("credential helper: %s", credHelper), nil } func deleteAuthFromCredHelper(credHelper, registry string) error { @@ -655,17 +674,17 @@ func deleteAuthFromCredHelper(credHelper, registry string) error { // findCredentialsInFile looks for credentials matching "key" // (which is "registry" or a namespace in "registry") in "path". -func findCredentialsInFile(key, registry, path string, legacyFormat bool) (types.DockerAuthConfig, error) { - auths, err := readJSONFile(path, legacyFormat) +func findCredentialsInFile(key, registry string, path authPath) (types.DockerAuthConfig, error) { + auths, err := path.parse() if err != nil { - return types.DockerAuthConfig{}, fmt.Errorf("reading JSON file %q: %w", path, err) + return types.DockerAuthConfig{}, fmt.Errorf("reading JSON file %q: %w", path.path, err) } // First try cred helpers. They should always be normalized. // This intentionally uses "registry", not "key"; we don't support namespaced // credentials in helpers. if ch, exists := auths.CredHelpers[registry]; exists { - logrus.Debugf("Looking up in credential helper %s based on credHelpers entry in %s", ch, path) + logrus.Debugf("Looking up in credential helper %s based on credHelpers entry in %s", ch, path.path) return getAuthFromCredHelper(ch, registry) } @@ -673,7 +692,7 @@ func findCredentialsInFile(key, registry, path string, legacyFormat bool) (types // (This is not a feature of ~/.docker/config.json; we support it even for // those files as an extension.) var keys []string - if !legacyFormat { + if !path.legacyFormat { keys = authKeysForKey(key) } else { keys = []string{registry} @@ -683,7 +702,7 @@ func findCredentialsInFile(key, registry, path string, legacyFormat bool) (types // keys we prefer exact matches as well. for _, key := range keys { if val, exists := auths.AuthConfigs[key]; exists { - return decodeDockerAuth(path, key, val) + return decodeDockerAuth(path.path, key, val) } } @@ -697,14 +716,14 @@ func findCredentialsInFile(key, registry, path string, legacyFormat bool) (types // so account for that as well. registry = normalizeRegistry(registry) for k, v := range auths.AuthConfigs { - if normalizeAuthFileKey(k, legacyFormat) == registry { - return decodeDockerAuth(path, k, v) + if normalizeAuthFileKey(k, path.legacyFormat) == registry { + return decodeDockerAuth(path.path, k, v) } } // Only log this if we found nothing; getCredentialsWithHomeDir logs the // source of found data. - logrus.Debugf("No credentials matching %s found in %s", key, path) + logrus.Debugf("No credentials matching %s found in %s", key, path.path) return types.DockerAuthConfig{}, nil } @@ -737,8 +756,8 @@ func decodeDockerAuth(path, key string, conf dockerAuthConfig) (types.DockerAuth return types.DockerAuthConfig{}, err } - parts := strings.SplitN(string(decoded), ":", 2) - if len(parts) != 2 { + user, passwordPart, valid := strings.Cut(string(decoded), ":") + if !valid { // if it's invalid just skip, as docker does if len(decoded) > 0 { // Docker writes "auths": { "$host": {} } entries if a credential helper is used, don’t warn about those logrus.Warnf(`Error parsing the "auth" field of a credential entry %q in %q, missing semicolon`, key, path) // Don’t include the text of decoded, because that might put secrets into a log. @@ -748,8 +767,7 @@ func decodeDockerAuth(path, key string, conf dockerAuthConfig) (types.DockerAuth return types.DockerAuthConfig{}, nil } - user := parts[0] - password := strings.Trim(parts[1], "\x00") + password := strings.Trim(passwordPart, "\x00") return types.DockerAuthConfig{ Username: user, Password: password, @@ -764,7 +782,7 @@ func normalizeAuthFileKey(key string, legacyFormat bool) string { stripped = strings.TrimPrefix(stripped, "https://") if legacyFormat || stripped != key { - stripped = strings.SplitN(stripped, "/", 2)[0] + stripped, _, _ = strings.Cut(stripped, "/") } return normalizeRegistry(stripped) diff --git a/pkg/docker/config/config_test.go b/pkg/docker/config/config_test.go index 4277bba824..82f036853c 100644 --- a/pkg/docker/config/config_test.go +++ b/pkg/docker/config/config_test.go @@ -26,29 +26,30 @@ func TestGetPathToAuth(t *testing.T) { tmpDir := t.TempDir() for caseIndex, c := range []struct { - sys *types.SystemContext - os string - xrd string - expected string - legacyFormat bool + sys *types.SystemContext + os string + xrd string + expected string + legacyFormat bool + expectedUserSpecified bool }{ // Default paths - {&types.SystemContext{}, linux, "", "/run/containers/" + uid + "/auth.json", false}, - {&types.SystemContext{}, darwin, "", darwinDefault, false}, - {nil, linux, "", "/run/containers/" + uid + "/auth.json", false}, - {nil, darwin, "", darwinDefault, false}, + {&types.SystemContext{}, linux, "", "/run/containers/" + uid + "/auth.json", false, false}, + {&types.SystemContext{}, darwin, "", darwinDefault, false, false}, + {nil, linux, "", "/run/containers/" + uid + "/auth.json", false, false}, + {nil, darwin, "", darwinDefault, false, false}, // SystemContext overrides - {&types.SystemContext{AuthFilePath: "/absolute/path"}, linux, "", "/absolute/path", false}, - {&types.SystemContext{AuthFilePath: "/absolute/path"}, darwin, "", "/absolute/path", false}, - {&types.SystemContext{LegacyFormatAuthFilePath: "/absolute/path"}, linux, "", "/absolute/path", true}, - {&types.SystemContext{LegacyFormatAuthFilePath: "/absolute/path"}, darwin, "", "/absolute/path", true}, - {&types.SystemContext{RootForImplicitAbsolutePaths: "/prefix"}, linux, "", "/prefix/run/containers/" + uid + "/auth.json", false}, - {&types.SystemContext{RootForImplicitAbsolutePaths: "/prefix"}, darwin, "", "/prefix/run/containers/" + uid + "/auth.json", false}, + {&types.SystemContext{AuthFilePath: "/absolute/path"}, linux, "", "/absolute/path", false, true}, + {&types.SystemContext{AuthFilePath: "/absolute/path"}, darwin, "", "/absolute/path", false, true}, + {&types.SystemContext{LegacyFormatAuthFilePath: "/absolute/path"}, linux, "", "/absolute/path", true, true}, + {&types.SystemContext{LegacyFormatAuthFilePath: "/absolute/path"}, darwin, "", "/absolute/path", true, true}, + {&types.SystemContext{RootForImplicitAbsolutePaths: "/prefix"}, linux, "", "/prefix/run/containers/" + uid + "/auth.json", false, false}, + {&types.SystemContext{RootForImplicitAbsolutePaths: "/prefix"}, darwin, "", "/prefix/run/containers/" + uid + "/auth.json", false, false}, // XDG_RUNTIME_DIR defined - {nil, linux, tmpDir, tmpDir + "/containers/auth.json", false}, - {nil, darwin, tmpDir, darwinDefault, false}, - {nil, linux, tmpDir + "/thisdoesnotexist", "", false}, - {nil, darwin, tmpDir + "/thisdoesnotexist", darwinDefault, false}, + {nil, linux, tmpDir, tmpDir + "/containers/auth.json", false, false}, + {nil, darwin, tmpDir, darwinDefault, false, false}, + {nil, linux, tmpDir + "/thisdoesnotexist", "", false, false}, + {nil, darwin, tmpDir + "/thisdoesnotexist", darwinDefault, false, false}, } { t.Run(fmt.Sprintf("%d", caseIndex), func(t *testing.T) { // Always use t.Setenv() to ensure XDG_RUNTIME_DIR is restored to the original value after the test. @@ -58,13 +59,13 @@ func TestGetPathToAuth(t *testing.T) { if c.xrd == "" { os.Unsetenv("XDG_RUNTIME_DIR") } - res, lf, err := getPathToAuthWithOS(c.sys, c.os) + res, userSpecified, err := getPathToAuthWithOS(c.sys, c.os) if c.expected == "" { assert.Error(t, err) } else { require.NoError(t, err) - assert.Equal(t, c.expected, res) - assert.Equal(t, c.legacyFormat, lf) + assert.Equal(t, authPath{path: c.expected, legacyFormat: c.legacyFormat}, res) + assert.Equal(t, c.expectedUserSpecified, userSpecified) } }) } @@ -652,7 +653,7 @@ func TestSetCredentials(t *testing.T) { } // Read the resulting file and verify it contains the expected keys - auth, err := readJSONFile(tmpFile.Name(), false) + auth, err := newAuthPathDefault(tmpFile.Name()).parse() require.NoError(t, err) assert.Len(t, auth.AuthConfigs, len(writtenCredentials)) // auth.AuthConfigs and writtenCredentials are both maps, i.e. their keys are unique; @@ -750,7 +751,6 @@ func TestRemoveAuthentication(t *testing.T) { }, }, } { - content, err := json.Marshal(&tc.config) require.NoError(t, err) @@ -772,7 +772,7 @@ func TestRemoveAuthentication(t *testing.T) { } } - auth, err := readJSONFile(tmpFile.Name(), false) + auth, err := newAuthPathDefault(tmpFile.Name()).parse() require.NoError(t, err) tc.assert(auth) @@ -856,7 +856,6 @@ func TestSetGetCredentials(t *testing.T) { useLegacyFormat: true, }, } { - // Create a new empty SystemContext referring an empty auth.json tmpFile, err := os.CreateTemp("", "auth.json-") require.NoError(t, err) diff --git a/pkg/shortnames/shortnames.go b/pkg/shortnames/shortnames.go index 8793711fea..eeb7c1effd 100644 --- a/pkg/shortnames/shortnames.go +++ b/pkg/shortnames/shortnames.go @@ -59,8 +59,6 @@ func parseUnnormalizedShortName(input string) (bool, reference.Named, error) { // the tag or digest and stores it in the return values so that both can be // re-added to a possible resolved alias' or USRs at a later point. func splitUserInput(named reference.Named) (isTagged bool, isDigested bool, normalized reference.Named, tag string, digest digest.Digest) { - normalized = named - tagged, isT := named.(reference.NamedTagged) if isT { isTagged = true @@ -170,7 +168,7 @@ func (r *Resolved) Description() string { // Note that nil is returned if len(pullErrors) == 0. Otherwise, the amount of // pull errors must equal the amount of pull candidates. func (r *Resolved) FormatPullErrors(pullErrors []error) error { - if len(pullErrors) >= 0 && len(pullErrors) != len(r.PullCandidates) { + if len(pullErrors) > 0 && len(pullErrors) != len(r.PullCandidates) { pullErrors = append(pullErrors, fmt.Errorf("internal error: expected %d instead of %d errors for %d pull candidates", len(r.PullCandidates), len(pullErrors), len(r.PullCandidates))) diff --git a/pkg/sysregistriesv2/shortnames.go b/pkg/sysregistriesv2/shortnames.go index 12939b24da..3a11542c62 100644 --- a/pkg/sysregistriesv2/shortnames.go +++ b/pkg/sysregistriesv2/shortnames.go @@ -14,6 +14,7 @@ import ( "github.com/containers/storage/pkg/homedir" "github.com/containers/storage/pkg/lockfile" "github.com/sirupsen/logrus" + "golang.org/x/exp/maps" ) // defaultShortNameMode is the default mode of registries.conf files if the @@ -308,9 +309,7 @@ func newShortNameAliasCache(path string, conf *shortNameAliasConf) (*shortNameAl // updateWithConfigurationFrom updates c with configuration from updates. // In case of conflict, updates is preferred. func (c *shortNameAliasCache) updateWithConfigurationFrom(updates *shortNameAliasCache) { - for name, value := range updates.namedAliases { - c.namedAliases[name] = value - } + maps.Copy(c.namedAliases, updates.namedAliases) } func loadShortNameAliasConf(confPath string) (*shortNameAliasConf, *shortNameAliasCache, error) { @@ -335,7 +334,7 @@ func loadShortNameAliasConf(confPath string) (*shortNameAliasConf, *shortNameAli return &conf, cache, nil } -func shortNameAliasesConfPathAndLock(ctx *types.SystemContext) (string, lockfile.Locker, error) { +func shortNameAliasesConfPathAndLock(ctx *types.SystemContext) (string, *lockfile.LockFile, error) { shortNameAliasesConfPath, err := shortNameAliasesConfPath(ctx) if err != nil { return "", nil, err @@ -346,6 +345,6 @@ func shortNameAliasesConfPathAndLock(ctx *types.SystemContext) (string, lockfile } lockPath := shortNameAliasesConfPath + ".lock" - locker, err := lockfile.GetLockfile(lockPath) + locker, err := lockfile.GetLockFile(lockPath) return shortNameAliasesConfPath, locker, err } diff --git a/pkg/sysregistriesv2/system_registries_v2.go b/pkg/sysregistriesv2/system_registries_v2.go index 41204dd9af..f45fd9de11 100644 --- a/pkg/sysregistriesv2/system_registries_v2.go +++ b/pkg/sysregistriesv2/system_registries_v2.go @@ -6,7 +6,6 @@ import ( "os" "path/filepath" "reflect" - "regexp" "sort" "strings" "sync" @@ -15,7 +14,9 @@ import ( "github.com/containers/image/v5/docker/reference" "github.com/containers/image/v5/types" "github.com/containers/storage/pkg/homedir" + "github.com/containers/storage/pkg/regexp" "github.com/sirupsen/logrus" + "golang.org/x/exp/maps" ) // systemRegistriesConfPath is the path to the system-wide registry @@ -198,6 +199,7 @@ type V1RegistriesConf struct { } // Nonempty returns true if config contains at least one configuration entry. +// Empty arrays are treated as missing entries. func (config *V1RegistriesConf) Nonempty() bool { copy := *config // A shallow copy if copy.V1TOMLConfig.Search.Registries != nil && len(copy.V1TOMLConfig.Search.Registries) == 0 { @@ -209,7 +211,15 @@ func (config *V1RegistriesConf) Nonempty() bool { if copy.V1TOMLConfig.Block.Registries != nil && len(copy.V1TOMLConfig.Block.Registries) == 0 { copy.V1TOMLConfig.Block.Registries = nil } - return !reflect.DeepEqual(copy, V1RegistriesConf{}) + return copy.hasSetField() +} + +// hasSetField returns true if config contains at least one configuration entry. +// This is useful because of a subtlety of the behavior of the TOML decoder, where a missing array field +// is not modified while unmarshaling (in our case remains to nil), while an [] is unmarshaled +// as a non-nil []string{}. +func (config *V1RegistriesConf) hasSetField() bool { + return !reflect.DeepEqual(*config, V1RegistriesConf{}) } // V2RegistriesConf is the sysregistries v2 configuration format. @@ -257,7 +267,15 @@ func (config *V2RegistriesConf) Nonempty() bool { if !copy.shortNameAliasConf.nonempty() { copy.shortNameAliasConf = shortNameAliasConf{} } - return !reflect.DeepEqual(copy, V2RegistriesConf{}) + return copy.hasSetField() +} + +// hasSetField returns true if config contains at least one configuration entry. +// This is useful because of a subtlety of the behavior of the TOML decoder, where a missing array field +// is not modified while unmarshaling (in our case remains to nil), while an [] is unmarshaled +// as a non-nil []string{}. +func (config *V2RegistriesConf) hasSetField() bool { + return !reflect.DeepEqual(*config, V2RegistriesConf{}) } // parsedConfig is the result of parsing, and possibly merging, configuration files; @@ -367,7 +385,7 @@ func (config *V1RegistriesConf) ConvertToV2() (*V2RegistriesConf, error) { } // anchoredDomainRegexp is an internal implementation detail of postProcess, defining the valid values of elements of UnqualifiedSearchRegistries. -var anchoredDomainRegexp = regexp.MustCompile("^" + reference.DomainRegexp.String() + "$") +var anchoredDomainRegexp = regexp.Delayed("^" + reference.DomainRegexp.String() + "$") // postProcess checks the consistency of all the configuration, looks for conflicts, // and normalizes the configuration (e.g., sets the Prefix to Location if not set). @@ -923,15 +941,15 @@ func loadConfigFile(path string, forceV2 bool) (*parsedConfig, error) { logrus.Debugf("Failed to decode keys %q from %q", keys, path) } - if combinedTOML.V1RegistriesConf.Nonempty() { + if combinedTOML.V1RegistriesConf.hasSetField() { // Enforce the v2 format if requested. if forceV2 { return nil, &InvalidRegistries{s: "registry must be in v2 format but is in v1"} } // Convert a v1 config into a v2 config. - if combinedTOML.V2RegistriesConf.Nonempty() { - return nil, &InvalidRegistries{s: "mixing sysregistry v1/v2 is not supported"} + if combinedTOML.V2RegistriesConf.hasSetField() { + return nil, &InvalidRegistries{s: fmt.Sprintf("mixing sysregistry v1/v2 is not supported: %#v", combinedTOML)} } converted, err := combinedTOML.V1RegistriesConf.ConvertToV2() if err != nil { @@ -1002,12 +1020,9 @@ func (c *parsedConfig) updateWithConfigurationFrom(updates *parsedConfig) { // Go maps have a non-deterministic order when iterating the keys, so // we dump them in a slice and sort it to enforce some order in // Registries slice. Some consumers of c/image (e.g., CRI-O) log the - // the configuration where a non-deterministic order could easily cause + // configuration where a non-deterministic order could easily cause // confusion. - prefixes := []string{} - for prefix := range registryMap { - prefixes = append(prefixes, prefix) - } + prefixes := maps.Keys(registryMap) sort.Strings(prefixes) c.partialV2.Registries = []Registry{} diff --git a/pkg/sysregistriesv2/system_registries_v2_test.go b/pkg/sysregistriesv2/system_registries_v2_test.go index 831a84b8f5..ca88c6ea78 100644 --- a/pkg/sysregistriesv2/system_registries_v2_test.go +++ b/pkg/sysregistriesv2/system_registries_v2_test.go @@ -12,54 +12,68 @@ import ( "github.com/stretchr/testify/require" ) +var v1RegistriesConfEmptyTestData = []struct { + nonempty, hasSetField bool + v V1RegistriesConf +}{ + {nonempty: false, hasSetField: false, v: V1RegistriesConf{}}, + {nonempty: false, hasSetField: true, v: V1RegistriesConf{V1TOMLConfig{Search: V1TOMLregistries{Registries: []string{}}}}}, + {nonempty: false, hasSetField: true, v: V1RegistriesConf{V1TOMLConfig{Insecure: V1TOMLregistries{Registries: []string{}}}}}, + {nonempty: false, hasSetField: true, v: V1RegistriesConf{V1TOMLConfig{Block: V1TOMLregistries{Registries: []string{}}}}}, + {nonempty: true, hasSetField: true, v: V1RegistriesConf{V1TOMLConfig{Search: V1TOMLregistries{Registries: []string{"example.com"}}}}}, + {nonempty: true, hasSetField: true, v: V1RegistriesConf{V1TOMLConfig{Insecure: V1TOMLregistries{Registries: []string{"example.com"}}}}}, + {nonempty: true, hasSetField: true, v: V1RegistriesConf{V1TOMLConfig{Block: V1TOMLregistries{Registries: []string{"example.com"}}}}}, +} + func TestV1RegistriesConfNonempty(t *testing.T) { - for _, c := range []V1RegistriesConf{ - {}, - {V1TOMLConfig: V1TOMLConfig{Search: V1TOMLregistries{Registries: []string{}}}}, - {V1TOMLConfig: V1TOMLConfig{Insecure: V1TOMLregistries{Registries: []string{}}}}, - {V1TOMLConfig: V1TOMLConfig{Block: V1TOMLregistries{Registries: []string{}}}}, - } { - copy := c // A shallow copy + for _, c := range v1RegistriesConfEmptyTestData { + copy := c.v // A shallow copy res := copy.Nonempty() - assert.False(t, res, c) - assert.Equal(t, c, copy, c) // Ensure the method did not change the original value + assert.Equal(t, c.nonempty, res, c.v) + assert.Equal(t, c.v, copy, c.v) // Ensure the method did not change the original value } - for _, c := range []V1RegistriesConf{ - {V1TOMLConfig: V1TOMLConfig{Search: V1TOMLregistries{Registries: []string{"example.com"}}}}, - {V1TOMLConfig: V1TOMLConfig{Insecure: V1TOMLregistries{Registries: []string{"example.com"}}}}, - {V1TOMLConfig: V1TOMLConfig{Block: V1TOMLregistries{Registries: []string{"example.com"}}}}, - } { - copy := c // A shallow copy - res := copy.Nonempty() - assert.True(t, res, c) - assert.Equal(t, c, copy, c) // Ensure the method did not change the original value +} + +func TestV1RegistriesConfHasSetField(t *testing.T) { + for _, c := range v1RegistriesConfEmptyTestData { + copy := c.v // A shallow copy + res := copy.hasSetField() + assert.Equal(t, c.hasSetField, res, c.v) + assert.Equal(t, c.v, copy, c.v) // Ensure the method did not change the original value } } +var v2RegistriesConfEmptyTestData = []struct { + nonempty, hasSetField bool + v V2RegistriesConf +}{ + {nonempty: false, hasSetField: false, v: V2RegistriesConf{}}, + {nonempty: false, hasSetField: true, v: V2RegistriesConf{Registries: []Registry{}}}, + {nonempty: false, hasSetField: true, v: V2RegistriesConf{UnqualifiedSearchRegistries: []string{}}}, + {nonempty: false, hasSetField: true, v: V2RegistriesConf{CredentialHelpers: []string{}}}, + {nonempty: false, hasSetField: true, v: V2RegistriesConf{shortNameAliasConf: shortNameAliasConf{Aliases: map[string]string{}}}}, + {nonempty: true, hasSetField: true, v: V2RegistriesConf{Registries: []Registry{{Prefix: "example.com"}}}}, + {nonempty: true, hasSetField: true, v: V2RegistriesConf{UnqualifiedSearchRegistries: []string{"example.com"}}}, + {nonempty: true, hasSetField: true, v: V2RegistriesConf{CredentialHelpers: []string{"a"}}}, + {nonempty: true, hasSetField: true, v: V2RegistriesConf{ShortNameMode: "enforcing"}}, + {nonempty: true, hasSetField: true, v: V2RegistriesConf{shortNameAliasConf: shortNameAliasConf{Aliases: map[string]string{"a": "example.com/b"}}}}, +} + func TestV2RegistriesConfNonempty(t *testing.T) { - for _, c := range []V2RegistriesConf{ - {}, - {Registries: []Registry{}}, - {UnqualifiedSearchRegistries: []string{}}, - {CredentialHelpers: []string{}}, - {shortNameAliasConf: shortNameAliasConf{Aliases: map[string]string{}}}, - } { - copy := c // A shallow copy + for _, c := range v2RegistriesConfEmptyTestData { + copy := c.v // A shallow copy res := copy.Nonempty() - assert.False(t, res, c) - assert.Equal(t, c, copy, c) // Ensure the method did not change the original value + assert.Equal(t, c.nonempty, res, c.v) + assert.Equal(t, c.v, copy, c.v) // Ensure the method did not change the original value } - for _, c := range []V2RegistriesConf{ - {Registries: []Registry{{Prefix: "example.com"}}}, - {UnqualifiedSearchRegistries: []string{"example.com"}}, - {CredentialHelpers: []string{"a"}}, - {ShortNameMode: "enforcing"}, - {shortNameAliasConf: shortNameAliasConf{Aliases: map[string]string{"a": "example.com/b"}}}, - } { - copy := c // A shallow copy - res := copy.Nonempty() - assert.True(t, res, c) - assert.Equal(t, c, copy, c) // Ensure the method did not change the original value +} + +func TestV2RegistriesConfHasSetField(t *testing.T) { + for _, c := range v2RegistriesConfEmptyTestData { + copy := c.v // A shallow copy + res := copy.hasSetField() + assert.Equal(t, c.hasSetField, res, c.v) + assert.Equal(t, c.v, copy, c.v) // Ensure the method did not change the original value } } @@ -475,11 +489,16 @@ func TestV1BackwardsCompatibility(t *testing.T) { } func TestMixingV1andV2(t *testing.T) { - _, err := GetRegistries(&types.SystemContext{ - SystemRegistriesConfPath: "testdata/mixing-v1-v2.conf", - SystemRegistriesConfDirPath: "testdata/this-does-not-exist", - }) - assert.ErrorContains(t, err, "mixing sysregistry v1/v2 is not supported") + for _, c := range []string{ + "testdata/mixing-v1-v2.conf", + "testdata/mixing-v1-v2-empty.conf", + } { + _, err := GetRegistries(&types.SystemContext{ + SystemRegistriesConfPath: c, + SystemRegistriesConfDirPath: "testdata/this-does-not-exist", + }) + assert.ErrorContains(t, err, "mixing sysregistry v1/v2 is not supported", c) + } } func TestConfigCache(t *testing.T) { @@ -631,124 +650,125 @@ func TestPullSourcesFromReference(t *testing.T) { SystemRegistriesConfDirPath: "testdata/this-does-not-exist", } registries, err := GetRegistries(sys) - assert.Nil(t, err) - assert.Equal(t, 2, len(registries)) - - // Registry A allowing any kind of pull from mirrors - registryA, err := FindRegistry(sys, "registry-a.com/foo/image:latest") - assert.Nil(t, err) - assert.NotNil(t, registryA) - // Digest - referenceADigest := toNamedRef(t, "registry-a.com/foo/image@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") - pullSources, err := registryA.PullSourcesFromReference(referenceADigest) - assert.Nil(t, err) - assert.Equal(t, 3, len(pullSources)) - assert.Equal(t, "mirror-1.registry-a.com/image@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", pullSources[0].Reference.String()) - assert.True(t, pullSources[1].Endpoint.Insecure) - // Tag - referenceATag := toNamedRef(t, "registry-a.com/foo/image:aaa") - pullSources, err = registryA.PullSourcesFromReference(referenceATag) - assert.Nil(t, err) - assert.Equal(t, 3, len(pullSources)) - assert.Equal(t, "registry-a.com/bar/image:aaa", pullSources[2].Reference.String()) - - // Registry B allowing digests pull only from mirrors - registryB, err := FindRegistry(sys, "registry-b.com/foo/image:latest") - assert.Nil(t, err) - assert.NotNil(t, registryB) - // Digest - referenceBDigest := toNamedRef(t, "registry-b.com/foo/image@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") - pullSources, err = registryB.PullSourcesFromReference(referenceBDigest) - assert.Nil(t, err) - assert.Equal(t, 3, len(pullSources)) - assert.Equal(t, "registry-b.com/bar", pullSources[2].Endpoint.Location) - assert.Equal(t, "registry-b.com/bar/image@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", pullSources[2].Reference.String()) - // Tag - referenceBTag := toNamedRef(t, "registry-b.com/foo/image:aaa") - pullSources, err = registryB.PullSourcesFromReference(referenceBTag) - assert.Nil(t, err) - assert.Equal(t, 1, len(pullSources)) -} - -func TestPullSourcesMirrorFromReference(t *testing.T) { - sys := &types.SystemContext{ - SystemRegistriesConfPath: "testdata/pull-sources-mirror-reference.conf", - SystemRegistriesConfDirPath: "testdata/this-does-not-exist", - } - registries, err := GetRegistries(sys) require.NoError(t, err) - assert.Equal(t, 7, len(registries)) + assert.Equal(t, 9, len(registries)) digest := "@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" tag := ":aaa" for _, tc := range []struct { - registry string - digestSources []string - tagSources []string + matchedPrefix string + repo string + digestPrefixes []string + digestInsecure []bool + tagPrefixes []string + tagInsecure []bool }{ + // Registry A allowing any kind of pull from mirrors + { + "registry-a.com/foo", + "image", + []string{"mirror-1.registry-a.com", "mirror-2.registry-a.com", "registry-a.com/bar"}, + []bool{false, true, false}, + []string{"mirror-1.registry-a.com", "mirror-2.registry-a.com", "registry-a.com/bar"}, + []bool{false, true, false}, + }, + // Registry B allowing digests pull only from mirrors + { + "registry-b.com/foo", + "image", + []string{"mirror-1.registry-b.com", "mirror-2.registry-b.com", "registry-b.com/bar"}, + []bool{false, false, false}, + []string{"registry-b.com/bar"}, + []bool{false}, + }, // Registry A has mirrors allow any kind of pull { - "registry-a.com/foo/image", + "registry-a.com/baz", + "image", []string{"mirror-1.registry-a.com", "mirror-2.registry-a.com", "registry-a.com/bar"}, + []bool{false, true, false}, []string{"mirror-1.registry-a.com", "mirror-2.registry-a.com", "registry-a.com/bar"}, + []bool{false, true, false}, }, // Registry B has mirrors allow digests pull only { - "registry-b.com/foo/image", + "registry-b.com/baz", + "image", []string{"mirror-1.registry-b.com", "mirror-2.registry-b.com", "registry-b.com/bar"}, + []bool{false, false, false}, []string{"registry-b.com/bar"}, + []bool{false}, }, // Registry C has a mirror allows digest pull only and a mirror allows any kind of pull { - "registry-c.com/foo/image", + "registry-c.com/baz", + "image", []string{"mirror-1.registry-c.com", "mirror-2.registry-c.com", "registry-c.com/bar"}, + []bool{false, false, false}, []string{"mirror-1.registry-c.com", "registry-c.com/bar"}, + []bool{false, false}, }, // Registry D set digest-only for registry level, allows only digest pulls // Registry D has no digest-only set for mirrors table { - "registry-d.com/foo/image", + "registry-d.com/baz", + "image", []string{"mirror-1.registry-d.com", "mirror-2.registry-d.com", "registry-d.com/bar"}, + []bool{false, false, false}, []string{"registry-d.com/bar"}, + []bool{false}, }, // Registry E has mirrors only allows tag pull { - "registry-e.com/foo/image", + "registry-e.com/baz", + "image", []string{"registry-e.com/bar"}, + []bool{false}, []string{"mirror-1.registry-e.com", "mirror-2.registry-e.com", "registry-e.com/bar"}, + []bool{false, false, false}, }, // Registry F has one tag only mirror does not allow digest pull { - "registry-f.com/foo/image", + "registry-f.com/baz", + "image", []string{"mirror-1.registry-f.com", "registry-f.com/bar"}, + []bool{false, false}, []string{"mirror-1.registry-f.com", "mirror-2.registry-f.com", "registry-f.com/bar"}, + []bool{false, false, false}, }, // Registry G has one digest-only pull and one tag only pull { - "registry-g.com/foo/image", + "registry-g.com/baz", + "image", []string{"mirror-1.registry-g.com", "mirror-3.registry-g.com", "mirror-4.registry-g.com", "registry-g.com/bar"}, + []bool{false, false, false, false}, []string{"mirror-2.registry-g.com", "mirror-3.registry-g.com", "mirror-4.registry-g.com", "registry-g.com/bar"}, + []bool{false, false, false, false}, }, } { // Digest - digestedRef := toNamedRef(t, tc.registry+digest) + digestedRef := toNamedRef(t, fmt.Sprintf("%s/%s", tc.matchedPrefix, tc.repo)+digest) registry, err := FindRegistry(sys, digestedRef.Name()) require.NoError(t, err) require.NotNil(t, registry) pullSource, err := registry.PullSourcesFromReference(digestedRef) require.NoError(t, err) - for i, s := range tc.digestSources { - assert.Equal(t, s, pullSource[i].Endpoint.Location) + for i, p := range tc.digestPrefixes { + assert.Equal(t, p, pullSource[i].Endpoint.Location) + assert.Equal(t, fmt.Sprintf("%s/%s", p, tc.repo)+digest, pullSource[i].Reference.String()) + assert.Equal(t, tc.digestInsecure[i], pullSource[i].Endpoint.Insecure) } // Tag - taggedRef := toNamedRef(t, tc.registry+tag) + taggedRef := toNamedRef(t, fmt.Sprintf("%s/%s", tc.matchedPrefix, tc.repo)+tag) registry, err = FindRegistry(sys, taggedRef.Name()) require.NoError(t, err) require.NotNil(t, registry) pullSource, err = registry.PullSourcesFromReference(taggedRef) require.NoError(t, err) - for i, s := range tc.tagSources { - assert.Equal(t, s, pullSource[i].Endpoint.Location) + for i, p := range tc.tagPrefixes { + assert.Equal(t, p, pullSource[i].Endpoint.Location) + assert.Equal(t, fmt.Sprintf("%s/%s", p, tc.repo)+tag, pullSource[i].Reference.String()) + assert.Equal(t, tc.tagInsecure[i], pullSource[i].Endpoint.Insecure) } } } diff --git a/pkg/sysregistriesv2/testdata/mixing-v1-v2-empty.conf b/pkg/sysregistriesv2/testdata/mixing-v1-v2-empty.conf new file mode 100644 index 0000000000..f049987b60 --- /dev/null +++ b/pkg/sysregistriesv2/testdata/mixing-v1-v2-empty.conf @@ -0,0 +1,10 @@ +unqualified-search-registries = [] + +[registries.search] +registries = [] + +[registries.block] +registries = [] + +[registries.insecure] +registries = [] diff --git a/pkg/sysregistriesv2/testdata/pull-sources-from-reference.conf b/pkg/sysregistriesv2/testdata/pull-sources-from-reference.conf index f01176dbae..ee50fdc2fc 100644 --- a/pkg/sysregistriesv2/testdata/pull-sources-from-reference.conf +++ b/pkg/sysregistriesv2/testdata/pull-sources-from-reference.conf @@ -19,3 +19,90 @@ location = "mirror-1.registry-b.com" [[registry.mirror]] location = "mirror-2.registry-b.com" + +[[registry]] +prefix = "registry-a.com/baz" +location = "registry-a.com/bar" + +[[registry.mirror]] +location = "mirror-1.registry-a.com" + +[[registry.mirror]] +location = "mirror-2.registry-a.com" +insecure = true + +[[registry]] +prefix = "registry-b.com/baz" +location = "registry-b.com/bar" + +[[registry.mirror]] +pull-from-mirror = "digest-only" +location = "mirror-1.registry-b.com" + +[[registry.mirror]] +pull-from-mirror = "digest-only" +location = "mirror-2.registry-b.com" + +[[registry]] +prefix = "registry-c.com/baz" +location = "registry-c.com/bar" + +[[registry.mirror]] +location = "mirror-1.registry-c.com" + +[[registry.mirror]] +pull-from-mirror = "digest-only" +location = "mirror-2.registry-c.com" + +[[registry]] +prefix = "registry-d.com/baz" +location = "registry-d.com/bar" +mirror-by-digest-only = true + +[[registry.mirror]] +location = "mirror-1.registry-d.com" + +[[registry.mirror]] +location = "mirror-2.registry-d.com" + +[[registry]] +prefix = "registry-e.com/baz" +location = "registry-e.com/bar" + +[[registry.mirror]] +pull-from-mirror = "tag-only" +location = "mirror-1.registry-e.com" + +[[registry.mirror]] +pull-from-mirror = "tag-only" +location = "mirror-2.registry-e.com" + +[[registry]] +prefix = "registry-f.com/baz" +location = "registry-f.com/bar" + +[[registry.mirror]] +location = "mirror-1.registry-f.com" + +[[registry.mirror]] +pull-from-mirror = "tag-only" +location = "mirror-2.registry-f.com" + +[[registry]] +prefix = "registry-g.com/baz" +location = "registry-g.com/bar" + +[[registry.mirror]] +pull-from-mirror = "digest-only" +location = "mirror-1.registry-g.com" + +[[registry.mirror]] +pull-from-mirror = "tag-only" +location = "mirror-2.registry-g.com" + +[[registry.mirror]] +location = "mirror-3.registry-g.com" + +[[registry.mirror]] +pull-from-mirror = "all" +location = "mirror-4.registry-g.com" diff --git a/pkg/sysregistriesv2/testdata/pull-sources-mirror-reference.conf b/pkg/sysregistriesv2/testdata/pull-sources-mirror-reference.conf deleted file mode 100644 index 50d51c2892..0000000000 --- a/pkg/sysregistriesv2/testdata/pull-sources-mirror-reference.conf +++ /dev/null @@ -1,86 +0,0 @@ -[[registry]] -prefix = "registry-a.com/foo" -location = "registry-a.com/bar" - -[[registry.mirror]] -location = "mirror-1.registry-a.com" - -[[registry.mirror]] -location = "mirror-2.registry-a.com" -insecure = true - -[[registry]] -prefix = "registry-b.com/foo" -location = "registry-b.com/bar" - -[[registry.mirror]] -pull-from-mirror = "digest-only" -location = "mirror-1.registry-b.com" - -[[registry.mirror]] -pull-from-mirror = "digest-only" -location = "mirror-2.registry-b.com" - -[[registry]] -prefix = "registry-c.com/foo" -location = "registry-c.com/bar" - -[[registry.mirror]] -location = "mirror-1.registry-c.com" - -[[registry.mirror]] -pull-from-mirror = "digest-only" -location = "mirror-2.registry-c.com" - -[[registry]] -prefix = "registry-d.com/foo" -location = "registry-d.com/bar" -mirror-by-digest-only = true - -[[registry.mirror]] -location = "mirror-1.registry-d.com" - -[[registry.mirror]] -location = "mirror-2.registry-d.com" - -[[registry]] -prefix = "registry-e.com/foo" -location = "registry-e.com/bar" - -[[registry.mirror]] -pull-from-mirror = "tag-only" -location = "mirror-1.registry-e.com" - -[[registry.mirror]] -pull-from-mirror = "tag-only" -location = "mirror-2.registry-e.com" - -[[registry]] -prefix = "registry-f.com/foo" -location = "registry-f.com/bar" - -[[registry.mirror]] -location = "mirror-1.registry-f.com" - -[[registry.mirror]] -pull-from-mirror = "tag-only" -location = "mirror-2.registry-f.com" - -[[registry]] -prefix = "registry-g.com/foo" -location = "registry-g.com/bar" - -[[registry.mirror]] -pull-from-mirror = "digest-only" -location = "mirror-1.registry-g.com" - -[[registry.mirror]] -pull-from-mirror = "tag-only" -location = "mirror-2.registry-g.com" - -[[registry.mirror]] -location = "mirror-3.registry-g.com" - -[[registry.mirror]] -pull-from-mirror = "all" -location = "mirror-4.registry-g.com" diff --git a/pkg/tlsclientconfig/tlsclientconfig.go b/pkg/tlsclientconfig/tlsclientconfig.go index 38bf976977..5301f192a9 100644 --- a/pkg/tlsclientconfig/tlsclientconfig.go +++ b/pkg/tlsclientconfig/tlsclientconfig.go @@ -2,6 +2,7 @@ package tlsclientconfig import ( "crypto/tls" + "crypto/x509" "fmt" "net" "net/http" @@ -10,9 +11,8 @@ import ( "strings" "time" - "github.com/docker/go-connections/sockets" - "github.com/docker/go-connections/tlsconfig" "github.com/sirupsen/logrus" + "golang.org/x/exp/slices" ) // SetupCertificates opens all .crt, .cert, and .key files in dir and appends / loads certs and key pairs as appropriate to tlsc @@ -47,7 +47,7 @@ func SetupCertificates(dir string, tlsc *tls.Config) error { return err } if tlsc.RootCAs == nil { - systemPool, err := tlsconfig.SystemCertPool() + systemPool, err := x509.SystemCertPool() if err != nil { return fmt.Errorf("unable to get system cert pool: %w", err) } @@ -81,12 +81,9 @@ func SetupCertificates(dir string, tlsc *tls.Config) error { } func hasFile(files []os.DirEntry, name string) bool { - for _, f := range files { - if f.Name() == name { - return true - } - } - return false + return slices.ContainsFunc(files, func(f os.DirEntry) bool { + return f.Name() == name + }) } // NewTransport Creates a default transport @@ -102,8 +99,5 @@ func NewTransport() *http.Transport { // TODO(dmcgowan): Call close idle connections when complete and use keep alive DisableKeepAlives: true, } - if _, err := sockets.DialerFromEnvironment(direct); err != nil { - logrus.Debugf("Can't execute DialerFromEnvironment: %v", err) - } return tr } diff --git a/pkg/tlsclientconfig/tlsclientconfig_test.go b/pkg/tlsclientconfig/tlsclientconfig_test.go index 3b65fe022f..8027814642 100644 --- a/pkg/tlsclientconfig/tlsclientconfig_test.go +++ b/pkg/tlsclientconfig/tlsclientconfig_test.go @@ -9,6 +9,7 @@ import ( "sort" "testing" + "github.com/containers/image/v5/internal/set" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -39,23 +40,23 @@ func TestSetupCertificates(t *testing.T) { // On systems where SystemCertPool is special cased, this compares two empty sets // and succeeds. // There isn’t a plausible alternative to calling .Subjects() here. - loadedSubjectBytes := map[string]struct{}{} + loadedSubjectBytes := set.New[string]() // lint:ignore SA1019 Receiving no data for system roots is acceptable. for _, s := range tlsc.RootCAs.Subjects() { //nolint staticcheck: the lint:ignore directive is somehow not recognized (and causes an extra warning!) - loadedSubjectBytes[string(s)] = struct{}{} + loadedSubjectBytes.Add(string(s)) } systemCertPool, err := x509.SystemCertPool() require.NoError(t, err) // lint:ignore SA1019 Receiving no data for system roots is acceptable. for _, s := range systemCertPool.Subjects() { //nolint staticcheck: the lint:ignore directive is somehow not recognized (and causes an extra warning!) - _, ok := loadedSubjectBytes[string(s)] + ok := loadedSubjectBytes.Contains(string(s)) assert.True(t, ok) } // RootCAs include our certificates. // We could possibly test without .Subjects() this by validating certificates // signed by our test CAs. - loadedSubjectCNs := map[string]struct{}{} + loadedSubjectCNs := set.New[string]() // lint:ignore SA1019 We only care about non-system roots here. for _, s := range tlsc.RootCAs.Subjects() { //nolint staticcheck: the lint:ignore directive is somehow not recognized (and causes an extra warning!) subjectRDN := pkix.RDNSequence{} @@ -64,11 +65,11 @@ func TestSetupCertificates(t *testing.T) { require.Empty(t, rest) subject := pkix.Name{} subject.FillFromRDNSequence(&subjectRDN) - loadedSubjectCNs[subject.CommonName] = struct{}{} + loadedSubjectCNs.Add(subject.CommonName) } - _, ok := loadedSubjectCNs["containers/image test CA certificate 1"] + ok := loadedSubjectCNs.Contains("containers/image test CA certificate 1") assert.True(t, ok) - _, ok = loadedSubjectCNs["containers/image test CA certificate 2"] + ok = loadedSubjectCNs.Contains("containers/image test CA certificate 2") assert.True(t, ok) // Certificates include our certificates require.Len(t, tlsc.Certificates, 2) diff --git a/sif/transport.go b/sif/transport.go index 2037f25082..4c09010714 100644 --- a/sif/transport.go +++ b/sif/transport.go @@ -87,7 +87,7 @@ func (ref sifReference) Transport() types.ImageTransport { // StringWithinTransport returns a string representation of the reference, which MUST be such that // reference.Transport().ParseReference(reference.StringWithinTransport()) returns an equivalent reference. // NOTE: The returned string is not promised to be equal to the original input to ParseReference; -// e.g. default attribute values omitted by the user may be filled in in the return value, or vice versa. +// e.g. default attribute values omitted by the user may be filled in the return value, or vice versa. // WARNING: Do not use the return value in the UI to describe an image, it does not contain the Transport().Name() prefix; // instead, see transports.ImageName(). func (ref sifReference) StringWithinTransport() string { diff --git a/signature/docker_test.go b/signature/docker_test.go index ba11bd1d5e..83b3fcd73e 100644 --- a/signature/docker_test.go +++ b/signature/docker_test.go @@ -2,21 +2,13 @@ package signature import ( "os" - "os/exec" "testing" + "github.com/containers/image/v5/internal/testing/gpgagent" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -// Kill the running gpg-agent to drop unlocked keys. This allows for testing handling of invalid passphrases. -func killGPGAgent(t *testing.T) { - cmd := exec.Command("gpgconf", "--kill", "gpg-agent") - cmd.Env = append(os.Environ(), "GNUPGHOME="+testGPGHomeDirectory) - err := cmd.Run() - assert.NoError(t, err) -} - func TestSignDockerManifest(t *testing.T) { mech, err := newGPGSigningMechanismInDirectory(testGPGHomeDirectory) require.NoError(t, err) @@ -54,7 +46,8 @@ func TestSignDockerManifest(t *testing.T) { } func TestSignDockerManifestWithPassphrase(t *testing.T) { - killGPGAgent(t) + err := gpgagent.KillGPGAgent(testGPGHomeDirectory) + require.NoError(t, err) mech, err := newGPGSigningMechanismInDirectory(testGPGHomeDirectory) require.NoError(t, err) diff --git a/signature/fixtures/cosign2.pub b/signature/fixtures/cosign2.pub new file mode 100644 index 0000000000..ca46da8f29 --- /dev/null +++ b/signature/fixtures/cosign2.pub @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEwOkOF9xpfG8ghueIhnZ66ooujwt1 ++ReV3HupgKnGFYnEh3Hh1YTg5L6kN1Yakkt5WltRoav8/R3hpCtUO3Rldw== +-----END PUBLIC KEY----- diff --git a/signature/fixtures/dir-img-cosign-fulcio-rekor-valid/manifest.json b/signature/fixtures/dir-img-cosign-fulcio-rekor-valid/manifest.json new file mode 100644 index 0000000000..b1ce3f6551 --- /dev/null +++ b/signature/fixtures/dir-img-cosign-fulcio-rekor-valid/manifest.json @@ -0,0 +1,16 @@ +{ + "schemaVersion": 2, + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "config": { + "mediaType": "application/vnd.docker.container.image.v1+json", + "size": 1508, + "digest": "sha256:3b0f78b718417dfa432fd8da26d0e3ac4ca3566d0825b0d2b056ccc4dd29e644" + }, + "layers": [ + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "size": 2568440, + "digest": "sha256:1df32bae7504a32024616c66017cd5df04dd98eaf150f8df45fffef2547a3c54" + } + ] +} \ No newline at end of file diff --git a/signature/fixtures/dir-img-cosign-fulcio-rekor-valid/signature-1 b/signature/fixtures/dir-img-cosign-fulcio-rekor-valid/signature-1 new file mode 100644 index 0000000000..332e0f950e Binary files /dev/null and b/signature/fixtures/dir-img-cosign-fulcio-rekor-valid/signature-1 differ diff --git a/signature/fixtures/dir-img-cosign-key-rekor-valid/manifest.json b/signature/fixtures/dir-img-cosign-key-rekor-valid/manifest.json new file mode 100644 index 0000000000..b1ce3f6551 --- /dev/null +++ b/signature/fixtures/dir-img-cosign-key-rekor-valid/manifest.json @@ -0,0 +1,16 @@ +{ + "schemaVersion": 2, + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "config": { + "mediaType": "application/vnd.docker.container.image.v1+json", + "size": 1508, + "digest": "sha256:3b0f78b718417dfa432fd8da26d0e3ac4ca3566d0825b0d2b056ccc4dd29e644" + }, + "layers": [ + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "size": 2568440, + "digest": "sha256:1df32bae7504a32024616c66017cd5df04dd98eaf150f8df45fffef2547a3c54" + } + ] +} \ No newline at end of file diff --git a/signature/fixtures/dir-img-cosign-key-rekor-valid/signature-1 b/signature/fixtures/dir-img-cosign-key-rekor-valid/signature-1 new file mode 100644 index 0000000000..befbf6c879 Binary files /dev/null and b/signature/fixtures/dir-img-cosign-key-rekor-valid/signature-1 differ diff --git a/signature/fixtures/fulcio-cert b/signature/fixtures/fulcio-cert new file mode 100644 index 0000000000..734b8bb200 --- /dev/null +++ b/signature/fixtures/fulcio-cert @@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE----- +MIICnTCCAiOgAwIBAgIUG45uaC2z8VvuOwzzm79RPfckoxwwCgYIKoZIzj0EAwMw +NzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRl +cm1lZGlhdGUwHhcNMjIxMjEyMTg0ODE4WhcNMjIxMjEyMTg1ODE4WjAAMFkwEwYH +KoZIzj0CAQYIKoZIzj0DAQcDQgAEJBurkhSRmHukHVh3VA8nYuSWqZ4ltafDwyWl +c3c9NaLovmu+NC4NiUtMLifL/P3nqedbnctKBuYmfISGiZlVyKOCAUIwggE+MA4G +A1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUAmvG +4K6lNk/sk/f4aiahjYRqR+cwHwYDVR0jBBgwFoAU39Ppz1YkEZb5qNjpKFWixi4Y +ZD8wHQYDVR0RAQH/BBMwEYEPbWl0ckByZWRoYXQuY29tMCwGCisGAQQBg78wAQEE +Hmh0dHBzOi8vZ2l0aHViLmNvbS9sb2dpbi9vYXV0aDCBiQYKKwYBBAHWeQIEAgR7 +BHkAdwB1AN09MGrGxxEyYxkeHJlnNwKiSl643jyt/4eKcoAvKe6OAAABhQeqlwUA +AAQDAEYwRAIgVi2SvN5ReGYiOShY8LFSRG5D6oATKqHb6kN/DJh2rAUCIANS6Aqp +xaXiHwTHUr3xPSGa5i6hywmbbRC6N0kIeDM6MAoGCCqGSM49BAMDA2gAMGUCMCLu +cZtESIN0lm64Co/bZ68CCRkWlktX4mmJiRhKi9c9QD62C5SuZhc0vvo6MOKmTQIx +AIo/MozZeoU7rdNj0pZKuBeCmMqmJaDSsw19tKi/b1pEmuw+Sf2BI25GgIbKztG1 +9w== +-----END CERTIFICATE----- diff --git a/signature/fixtures/fulcio-chain b/signature/fixtures/fulcio-chain new file mode 100644 index 0000000000..1c1e9f764b --- /dev/null +++ b/signature/fixtures/fulcio-chain @@ -0,0 +1,27 @@ +-----BEGIN CERTIFICATE----- +MIICGjCCAaGgAwIBAgIUALnViVfnU0brJasmRkHrn/UnfaQwCgYIKoZIzj0EAwMw +KjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0y +MjA0MTMyMDA2MTVaFw0zMTEwMDUxMzU2NThaMDcxFTATBgNVBAoTDHNpZ3N0b3Jl +LmRldjEeMBwGA1UEAxMVc2lnc3RvcmUtaW50ZXJtZWRpYXRlMHYwEAYHKoZIzj0C +AQYFK4EEACIDYgAE8RVS/ysH+NOvuDZyPIZtilgUF9NlarYpAd9HP1vBBH1U5CV7 +7LSS7s0ZiH4nE7Hv7ptS6LvvR/STk798LVgMzLlJ4HeIfF3tHSaexLcYpSASr1kS +0N/RgBJz/9jWCiXno3sweTAOBgNVHQ8BAf8EBAMCAQYwEwYDVR0lBAwwCgYIKwYB +BQUHAwMwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU39Ppz1YkEZb5qNjp +KFWixi4YZD8wHwYDVR0jBBgwFoAUWMAeX5FFpWapesyQoZMi0CrFxfowCgYIKoZI +zj0EAwMDZwAwZAIwPCsQK4DYiZYDPIaDi5HFKnfxXx6ASSVmERfsynYBiX2X6SJR +nZU84/9DZdnFvvxmAjBOt6QpBlc4J/0DxvkTCqpclvziL6BCCPnjdlIB3Pu3BxsP +mygUY7Ii2zbdCdliiow= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIB9zCCAXygAwIBAgIUALZNAPFdxHPwjeDloDwyYChAO/4wCgYIKoZIzj0EAwMw +KjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0y +MTEwMDcxMzU2NTlaFw0zMTEwMDUxMzU2NThaMCoxFTATBgNVBAoTDHNpZ3N0b3Jl +LmRldjERMA8GA1UEAxMIc2lnc3RvcmUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAT7 +XeFT4rb3PQGwS4IajtLk3/OlnpgangaBclYpsYBr5i+4ynB07ceb3LP0OIOZdxex +X69c5iVuyJRQ+Hz05yi+UF3uBWAlHpiS5sh0+H2GHE7SXrk1EC5m1Tr19L9gg92j +YzBhMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRY +wB5fkUWlZql6zJChkyLQKsXF+jAfBgNVHSMEGDAWgBRYwB5fkUWlZql6zJChkyLQ +KsXF+jAKBggqhkjOPQQDAwNpADBmAjEAj1nHeXZp+13NWBNa+EDsDP8G1WWg1tCM +WP/WHPqpaVo0jhsweNFZgSs0eE7wYI4qAjEA2WB9ot98sIkoF3vZYdd3/VtWB5b9 +TNMea7Ix/stJ5TfcLLeABLE4BNJOsQ4vnBHJ +-----END CERTIFICATE----- \ No newline at end of file diff --git a/signature/fixtures/fulcio_v1.crt.pem b/signature/fixtures/fulcio_v1.crt.pem new file mode 100644 index 0000000000..3afc46bb6e --- /dev/null +++ b/signature/fixtures/fulcio_v1.crt.pem @@ -0,0 +1,13 @@ +-----BEGIN CERTIFICATE----- +MIIB9zCCAXygAwIBAgIUALZNAPFdxHPwjeDloDwyYChAO/4wCgYIKoZIzj0EAwMw +KjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0y +MTEwMDcxMzU2NTlaFw0zMTEwMDUxMzU2NThaMCoxFTATBgNVBAoTDHNpZ3N0b3Jl +LmRldjERMA8GA1UEAxMIc2lnc3RvcmUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAT7 +XeFT4rb3PQGwS4IajtLk3/OlnpgangaBclYpsYBr5i+4ynB07ceb3LP0OIOZdxex +X69c5iVuyJRQ+Hz05yi+UF3uBWAlHpiS5sh0+H2GHE7SXrk1EC5m1Tr19L9gg92j +YzBhMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRY +wB5fkUWlZql6zJChkyLQKsXF+jAfBgNVHSMEGDAWgBRYwB5fkUWlZql6zJChkyLQ +KsXF+jAKBggqhkjOPQQDAwNpADBmAjEAj1nHeXZp+13NWBNa+EDsDP8G1WWg1tCM +WP/WHPqpaVo0jhsweNFZgSs0eE7wYI4qAjEA2WB9ot98sIkoF3vZYdd3/VtWB5b9 +TNMea7Ix/stJ5TfcLLeABLE4BNJOsQ4vnBHJ +-----END CERTIFICATE----- \ No newline at end of file diff --git a/signature/fixtures/rekor-payload b/signature/fixtures/rekor-payload new file mode 120000 index 0000000000..04a25636bd --- /dev/null +++ b/signature/fixtures/rekor-payload @@ -0,0 +1 @@ +../internal/testdata/rekor-payload \ No newline at end of file diff --git a/signature/fixtures/rekor-set b/signature/fixtures/rekor-set new file mode 120000 index 0000000000..5cd8159dfc --- /dev/null +++ b/signature/fixtures/rekor-set @@ -0,0 +1 @@ +../internal/testdata/rekor-set \ No newline at end of file diff --git a/signature/fixtures/rekor-sig b/signature/fixtures/rekor-sig new file mode 120000 index 0000000000..16b66d7bb8 --- /dev/null +++ b/signature/fixtures/rekor-sig @@ -0,0 +1 @@ +../internal/testdata/rekor-sig \ No newline at end of file diff --git a/signature/fixtures/rekor.pub b/signature/fixtures/rekor.pub new file mode 120000 index 0000000000..ac451b9d9a --- /dev/null +++ b/signature/fixtures/rekor.pub @@ -0,0 +1 @@ +../internal/testdata/rekor.pub \ No newline at end of file diff --git a/signature/fixtures/some-rsa-key.pub b/signature/fixtures/some-rsa-key.pub new file mode 100644 index 0000000000..1d56ec17b5 --- /dev/null +++ b/signature/fixtures/some-rsa-key.pub @@ -0,0 +1,14 @@ +-----BEGIN PUBLIC KEY----- +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA1bofelEsr9ZYEDlNZN+Z +K4LbdXi1grcUYGk9S7UsaZV4ovq++j5yYAiM2mWBxcmD65pokyanLEz/0ya9zSez +C1dY337ecoRj7OgLzrMJarKJu3xDYe/jCylQZCke7bobsIahi00i6sU0Feh94ULt +aSiBaOEZzYvbE6QBsfQWwH2EC/Ch0Dsy6zpZQxnsEGJkpr/ed9UYwUlgaGAouL4N +A5flQvDNWyKNMBsosrTHE+yRYiouAytszJ1WzMQrR8n3ngsMaJtM5FZClyT1FbB5 +YnvoFcTUqVQqMFCK6DBQvMp5mOBHAdWaHXTAP9cKFK73CdQwHigP3ayXRyq/Npjd +tSpk1AW12Ctkiu9jIX1upGpwJCz0nevsXm5CL2jdxbzDPxIHV4w4kHxt6CcCWJ3V +DSqTIV8xGMyRahUsmhnRXeRSF0UaQdzI9ZPKWW20bYjZ3qJawO3YhJZUwxZKiAm6 +Px2nd4lVpJXZLZ/DJ7eXJ0rWiwC2Y3C+1FlXdWtbocersg6a7oW/VNe+unVznHWm +N0GSp1IobRsvP6t5ITIJiQguROl8PVNS7Wu4vzDndy1cH4NBCyrQopQLJ29U51S4 +2tqYlSlJReWjvSidCSZDa9/YXU9LZqVWmkSKwMcCKCRACIYOLxFZJjsxj5hOIOkT +4EeUfiK04GAV1QKVloZ9b2sCAwEAAQ== +-----END PUBLIC KEY----- diff --git a/signature/fulcio_cert.go b/signature/fulcio_cert.go new file mode 100644 index 0000000000..cb1ca045e0 --- /dev/null +++ b/signature/fulcio_cert.go @@ -0,0 +1,167 @@ +package signature + +import ( + "crypto" + "crypto/ecdsa" + "crypto/x509" + "encoding/asn1" + "errors" + "fmt" + "time" + + "github.com/containers/image/v5/signature/internal" + "github.com/sigstore/fulcio/pkg/certificate" + "github.com/sigstore/sigstore/pkg/cryptoutils" + "golang.org/x/exp/slices" +) + +// fulcioTrustRoot contains policy allow validating Fulcio-issued certificates. +// Users should call validate() on the policy before using it. +type fulcioTrustRoot struct { + caCertificates *x509.CertPool + oidcIssuer string + subjectEmail string +} + +func (f *fulcioTrustRoot) validate() error { + if f.oidcIssuer == "" { + return errors.New("Internal inconsistency: Fulcio use set up without OIDC issuer") + } + if f.subjectEmail == "" { + return errors.New("Internal inconsistency: Fulcio use set up without subject email") + } + return nil +} + +func (f *fulcioTrustRoot) verifyFulcioCertificateAtTime(relevantTime time.Time, untrustedCertificateBytes []byte, untrustedIntermediateChainBytes []byte) (crypto.PublicKey, error) { + // == Verify the certificate is correctly signed + var untrustedIntermediatePool *x509.CertPool // = nil + // untrustedCertificateChainPool.AppendCertsFromPEM does something broadly similar, + // but it seems to optimize for memory usage at the cost of larger CPU usage (i.e. to load + // the hundreds of trusted CAs). Golang’s TLS code similarly calls individual AddCert + // for intermediate certificates. + if len(untrustedIntermediateChainBytes) > 0 { + untrustedIntermediateChain, err := cryptoutils.UnmarshalCertificatesFromPEM(untrustedIntermediateChainBytes) + if err != nil { + return nil, internal.NewInvalidSignatureError(fmt.Sprintf("loading certificate chain: %v", err)) + } + untrustedIntermediatePool = x509.NewCertPool() + if len(untrustedIntermediateChain) > 1 { + for _, untrustedIntermediateCert := range untrustedIntermediateChain[:len(untrustedIntermediateChain)-1] { + untrustedIntermediatePool.AddCert(untrustedIntermediateCert) + } + } + } + + untrustedLeafCerts, err := cryptoutils.UnmarshalCertificatesFromPEM(untrustedCertificateBytes) + if err != nil { + return nil, internal.NewInvalidSignatureError(fmt.Sprintf("parsing leaf certificate: %v", err)) + } + switch len(untrustedLeafCerts) { + case 0: + return nil, internal.NewInvalidSignatureError("no certificate found in signature certificate data") + case 1: + break // OK + default: + return nil, internal.NewInvalidSignatureError("unexpected multiple certificates present in signature certificate data") + } + untrustedCertificate := untrustedLeafCerts[0] + + // Go rejects Subject Alternative Name that has no DNSNames, EmailAddresses, IPAddresses and URIs; + // we match SAN ourselves, so override that. + if len(untrustedCertificate.UnhandledCriticalExtensions) > 0 { + var remaining []asn1.ObjectIdentifier + for _, oid := range untrustedCertificate.UnhandledCriticalExtensions { + if !oid.Equal(cryptoutils.SANOID) { + remaining = append(remaining, oid) + } + } + untrustedCertificate.UnhandledCriticalExtensions = remaining + } + + if _, err := untrustedCertificate.Verify(x509.VerifyOptions{ + Intermediates: untrustedIntermediatePool, + Roots: f.caCertificates, + // NOTE: Cosign uses untrustedCertificate.NotBefore here (i.e. uses _that_ time for intermediate certificate validation), + // and validates the leaf certificate against relevantTime manually. + // We verify the full certificate chain against relevantTime instead. + // Assuming the certificate is fulcio-generated and very short-lived, that should make little difference. + CurrentTime: relevantTime, + KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageCodeSigning}, + }); err != nil { + return nil, internal.NewInvalidSignatureError(fmt.Sprintf("veryfing leaf certificate failed: %v", err)) + } + + // Cosign verifies a SCT of the certificate (either embedded, or even, probably irrelevant, externally-supplied). + // + // We don’t currently do that. + // + // At the very least, with Fulcio we require Rekor SETs to prove Rekor contains a log of the signature, and that + // already contains the full certificate; so a SCT of the certificate is superfluous (assuming Rekor allowed searching by + // certificate subject, which, well…). That argument might go away if we add support for RFC 3161 timestamps instead of Rekor. + // + // Secondarily, assuming a trusted Fulcio server (which, to be fair, might not be the case for the public one) SCT is not clearly + // better than the Fulcio server maintaining an audit log; a SCT can only reveal a misissuance if there is some other authoritative + // log of approved Fulcio invocations, and it’s not clear where that would come from, especially human users manually + // logging in using OpenID are not going to maintain a record of those actions. + // + // Also, the SCT does not help reveal _what_ was maliciously signed, nor does it protect against malicious signatures + // by correctly-issued certificates. + // + // So, pragmatically, the ideal design seem to be to only do signatures from a trusted build system (which is, by definition, + // the arbiter of desired vs. malicious signatures) that maintains an audit log of performed signature operations; and that seems to + // make the SCT (and all of Rekor apart from the trusted timestamp) unnecessary. + + // == Validate the recorded OIDC issuer + gotOIDCIssuer := false + var oidcIssuer string + // certificate.ParseExtensions doesn’t reject duplicate extensions. + // Go 1.19 rejects duplicate extensions universally; but until we can require Go 1.19, + // reject duplicates manually. With Go 1.19, we could call certificate.ParseExtensions again. + for _, untrustedExt := range untrustedCertificate.Extensions { + if untrustedExt.Id.Equal(certificate.OIDIssuer) { + if gotOIDCIssuer { + // Coverage: This is unreachable in Go ≥1.19, which rejects certificates with duplicate extensions + // already in ParseCertificate. + return nil, internal.NewInvalidSignatureError("Fulcio certificate has a duplicate OIDC issuer extension") + } + oidcIssuer = string(untrustedExt.Value) + gotOIDCIssuer = true + } + } + if !gotOIDCIssuer { + return nil, internal.NewInvalidSignatureError("Fulcio certificate is missing the issuer extension") + } + if oidcIssuer != f.oidcIssuer { + return nil, internal.NewInvalidSignatureError(fmt.Sprintf("Unexpected Fulcio OIDC issuer %q", oidcIssuer)) + } + + // == Validate the OIDC subject + if !slices.Contains(untrustedCertificate.EmailAddresses, f.subjectEmail) { + return nil, internal.NewInvalidSignatureError(fmt.Sprintf("Required email %s not found (got %#v)", + f.subjectEmail, + untrustedCertificate.EmailAddresses)) + } + // FIXME: Match more subject types? Cosign does: + // - .DNSNames (can’t be issued by Fulcio) + // - .IPAddresses (can’t be issued by Fulcio) + // - .URIs (CAN be issued by Fulcio) + // - OtherName values in SAN (CAN be issued by Fulcio) + // - Various values about GitHub workflows (CAN be issued by Fulcio) + // What does it… mean to get an OAuth2 identity for an IP address? + // FIXME: How far into Turing-completeness for the issuer/subject do we need to get? Simultaneously accepted alternatives, for + // issuers and/or subjects and/or combinations? Regexps? More? + + return untrustedCertificate.PublicKey, nil +} + +func verifyRekorFulcio(rekorPublicKey *ecdsa.PublicKey, fulcioTrustRoot *fulcioTrustRoot, untrustedRekorSET []byte, + untrustedCertificateBytes []byte, untrustedIntermediateChainBytes []byte, untrustedBase64Signature string, + untrustedPayloadBytes []byte) (crypto.PublicKey, error) { + rekorSETTime, err := internal.VerifyRekorSET(rekorPublicKey, untrustedRekorSET, untrustedCertificateBytes, + untrustedBase64Signature, untrustedPayloadBytes) + if err != nil { + return nil, err + } + return fulcioTrustRoot.verifyFulcioCertificateAtTime(rekorSETTime, untrustedCertificateBytes, untrustedIntermediateChainBytes) +} diff --git a/signature/fulcio_cert_test.go b/signature/fulcio_cert_test.go new file mode 100644 index 0000000000..e81e31effc --- /dev/null +++ b/signature/fulcio_cert_test.go @@ -0,0 +1,342 @@ +package signature + +import ( + "bytes" + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "encoding/asn1" + "encoding/pem" + "os" + "testing" + "time" + + "github.com/sigstore/fulcio/pkg/certificate" + "github.com/sigstore/sigstore/pkg/cryptoutils" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// assert that crypto.PublicKey matches the on in certPEM. +func assertPublicKeyMatchesCert(t *testing.T, certPEM []byte, pk crypto.PublicKey) { + pkInterface, ok := pk.(interface { + Equal(x crypto.PublicKey) bool + }) + require.True(t, ok) + certs, err := cryptoutils.UnmarshalCertificatesFromPEM(certPEM) + require.NoError(t, err) + require.Len(t, certs, 1) + equal := pkInterface.Equal(certs[0].PublicKey) + assert.True(t, equal) +} + +func TestFulcioTrustRootValidate(t *testing.T) { + certs := x509.NewCertPool() // Empty is valid enough for our purposes. + + for _, tr := range []fulcioTrustRoot{ + { + caCertificates: certs, + oidcIssuer: "", + subjectEmail: "email", + }, + { + caCertificates: certs, + oidcIssuer: "issuer", + subjectEmail: "", + }, + } { + err := tr.validate() + assert.Error(t, err) + } + + tr := fulcioTrustRoot{ + caCertificates: certs, + oidcIssuer: "issuer", + subjectEmail: "email", + } + err := tr.validate() + assert.NoError(t, err) +} + +func TestFulcioTrustRootVerifyFulcioCertificateAtTime(t *testing.T) { + fulcioCACertificates := x509.NewCertPool() + fulcioCABundlePEM, err := os.ReadFile("fixtures/fulcio_v1.crt.pem") + require.NoError(t, err) + ok := fulcioCACertificates.AppendCertsFromPEM(fulcioCABundlePEM) + require.True(t, ok) + fulcioCertBytes, err := os.ReadFile("fixtures/fulcio-cert") + require.NoError(t, err) + fulcioChainBytes, err := os.ReadFile("fixtures/fulcio-chain") + require.NoError(t, err) + + // A successful verification + tr := fulcioTrustRoot{ + caCertificates: fulcioCACertificates, + oidcIssuer: "https://github.com/login/oauth", + subjectEmail: "mitr@redhat.com", + } + pk, err := tr.verifyFulcioCertificateAtTime(time.Unix(1670870899, 0), fulcioCertBytes, fulcioChainBytes) + require.NoError(t, err) + assertPublicKeyMatchesCert(t, fulcioCertBytes, pk) + + // Invalid intermediate certificates + pk, err = tr.verifyFulcioCertificateAtTime(time.Unix(1670870899, 0), fulcioCertBytes, []byte("not a certificate")) + assert.Error(t, err) + assert.Nil(t, pk) + + // No intermediate certificates: verification fails as is … + pk, err = tr.verifyFulcioCertificateAtTime(time.Unix(1670870899, 0), fulcioCertBytes, []byte{}) + assert.Error(t, err) + assert.Nil(t, pk) + // … but succeeds if we add the intermediate certificates to the root of trust + intermediateCertPool := x509.NewCertPool() + ok = intermediateCertPool.AppendCertsFromPEM(fulcioChainBytes) + require.True(t, ok) + trWithIntermediates := fulcioTrustRoot{ + caCertificates: intermediateCertPool, + oidcIssuer: "https://github.com/login/oauth", + subjectEmail: "mitr@redhat.com", + } + pk, err = trWithIntermediates.verifyFulcioCertificateAtTime(time.Unix(1670870899, 0), fulcioCertBytes, []byte{}) + require.NoError(t, err) + assertPublicKeyMatchesCert(t, fulcioCertBytes, pk) + + // Invalid leaf certificate + for _, c := range [][]byte{ + []byte("not a certificate"), + {}, // Empty + bytes.Repeat(fulcioCertBytes, 2), // More than one certificate + } { + pk, err := tr.verifyFulcioCertificateAtTime(time.Unix(1670870899, 0), c, fulcioChainBytes) + assert.Error(t, err) + assert.Nil(t, pk) + } + + // Unexpected relevantTime + for _, tm := range []time.Time{ + time.Date(2022, time.December, 12, 18, 48, 17, 0, time.UTC), + time.Date(2022, time.December, 12, 18, 58, 19, 0, time.UTC), + } { + pk, err := tr.verifyFulcioCertificateAtTime(tm, fulcioCertBytes, fulcioChainBytes) + assert.Error(t, err) + assert.Nil(t, pk) + } + + referenceTime := time.Now() + testCAKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + testCASN, err := cryptoutils.GenerateSerialNumber() + require.NoError(t, err) + testCAContents := x509.Certificate{ + SerialNumber: testCASN, + Subject: pkix.Name{CommonName: "root CA"}, + NotBefore: referenceTime.Add(-1 * time.Minute), + NotAfter: referenceTime.Add(1 * time.Hour), + BasicConstraintsValid: true, + IsCA: true, + } + testCACertBytes, err := x509.CreateCertificate(rand.Reader, &testCAContents, &testCAContents, + testCAKey.Public(), testCAKey) + require.NoError(t, err) + testCACert, err := x509.ParseCertificate(testCACertBytes) + require.NoError(t, err) + testCACertPool := x509.NewCertPool() + testCACertPool.AddCert(testCACert) + + for _, c := range []struct { + name string + fn func(cert *x509.Certificate) + errorFragment string + }{ + { + // OtherName SAN element, with none of the Go-parsed SAN elements present, + // should not be a reason to reject the certificate entirely; + // but we don’t actually support matching it, so this basically tests that the code + // gets far enough to do subject matching. + name: "OtherName in SAN", + fn: func(cert *x509.Certificate) { + // Setting SAN in ExtraExtensions causes EmailAddresses to be ignored, + // so we need to construct the whole SAN manually. + sansBytes, err := asn1.Marshal([]asn1.RawValue{ + { + Class: 2, + Tag: 0, + IsCompound: false, + Bytes: []byte("otherName"), + }, + }) + require.NoError(t, err) + cert.ExtraExtensions = append(cert.ExtraExtensions, pkix.Extension{ + Id: cryptoutils.SANOID, + Critical: true, + Value: sansBytes, + }) + }, + errorFragment: "Required email test-user@example.com not found", + }, + { // Other completely unrecognized critical extensions still cause failures + name: "Unhandled critical extension", + fn: func(cert *x509.Certificate) { + cert.ExtraExtensions = append(cert.ExtraExtensions, pkix.Extension{ + Id: asn1.ObjectIdentifier{2, 99999, 99998, 99997, 99996}, + Critical: true, + Value: []byte("whatever"), + }) + }, + errorFragment: "unhandled critical extension", + }, + { + name: "Missing issuer", + fn: func(cert *x509.Certificate) { + cert.ExtraExtensions = nil // Remove the issuer extension + }, + errorFragment: "Fulcio certificate is missing the issuer extension", + }, + { + name: "Duplicate issuer extension", + fn: func(cert *x509.Certificate) { + cert.ExtraExtensions = append([]pkix.Extension{ + { + Id: certificate.OIDIssuer, + Value: []byte("this does not match"), + }, + }, cert.ExtraExtensions...) + }, + // Match both our message and the Go 1.19 message: "certificate contains duplicate extensions" + errorFragment: "duplicate", + }, + { + name: "Issuer mismatch", + fn: func(cert *x509.Certificate) { + cert.ExtraExtensions = []pkix.Extension{ + { + Id: certificate.OIDIssuer, + Value: []byte("this does not match"), + }, + } + }, + errorFragment: "Unexpected Fulcio OIDC issuer", + }, + { + name: "Missing subject email", + fn: func(cert *x509.Certificate) { + cert.EmailAddresses = nil + }, + errorFragment: "Required email test-user@example.com not found", + }, + { + name: "Multiple emails, one matches", + fn: func(cert *x509.Certificate) { + cert.EmailAddresses = []string{"a@example.com", "test-user@example.com", "c@example.com"} + }, + errorFragment: "", + }, + { + name: "Email mismatch", + fn: func(cert *x509.Certificate) { + cert.EmailAddresses = []string{"a@example.com"} + }, + errorFragment: "Required email test-user@example.com not found", + }, + { + name: "Multiple emails, no matches", + fn: func(cert *x509.Certificate) { + cert.EmailAddresses = []string{"a@example.com", "b@example.com", "c@example.com"} + }, + errorFragment: "Required email test-user@example.com not found", + }, + } { + testLeafKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err, c.name) + testLeafSN, err := cryptoutils.GenerateSerialNumber() + require.NoError(t, err, c.name) + testLeafContents := x509.Certificate{ + SerialNumber: testLeafSN, + Subject: pkix.Name{CommonName: "leaf"}, + NotBefore: referenceTime.Add(-1 * time.Minute), + NotAfter: referenceTime.Add(1 * time.Hour), + ExtraExtensions: []pkix.Extension{ + { + Id: certificate.OIDIssuer, + Value: []byte("https://github.com/login/oauth"), + }, + }, + EmailAddresses: []string{"test-user@example.com"}, + } + c.fn(&testLeafContents) + testLeafCert, err := x509.CreateCertificate(rand.Reader, &testLeafContents, testCACert, testLeafKey.Public(), testCAKey) + require.NoError(t, err, c.name) + tr := fulcioTrustRoot{ + caCertificates: testCACertPool, + oidcIssuer: "https://github.com/login/oauth", + subjectEmail: "test-user@example.com", + } + testLeafPEM := pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Bytes: testLeafCert, + }) + pk, err := tr.verifyFulcioCertificateAtTime(referenceTime, testLeafPEM, []byte{}) + if c.errorFragment == "" { + require.NoError(t, err, c.name) + assertPublicKeyMatchesCert(t, testLeafPEM, pk) + } else { + assert.ErrorContains(t, err, c.errorFragment, c.name) + assert.Nil(t, pk, c.name) + } + } + +} + +func TestVerifyRekorFulcio(t *testing.T) { + caCertificates := x509.NewCertPool() + fulcioCABundlePEM, err := os.ReadFile("fixtures/fulcio_v1.crt.pem") + require.NoError(t, err) + ok := caCertificates.AppendCertsFromPEM(fulcioCABundlePEM) + require.True(t, ok) + certBytes, err := os.ReadFile("fixtures/fulcio-cert") + require.NoError(t, err) + chainBytes, err := os.ReadFile("fixtures/fulcio-chain") + require.NoError(t, err) + rekorKeyPEM, err := os.ReadFile("fixtures/rekor.pub") + require.NoError(t, err) + rekorKey, err := cryptoutils.UnmarshalPEMToPublicKey(rekorKeyPEM) + require.NoError(t, err) + rekorKeyECDSA, ok := rekorKey.(*ecdsa.PublicKey) + require.True(t, ok) + setBytes, err := os.ReadFile("fixtures/rekor-set") + require.NoError(t, err) + sigBase64, err := os.ReadFile("fixtures/rekor-sig") + require.NoError(t, err) + payloadBytes, err := os.ReadFile("fixtures/rekor-payload") + require.NoError(t, err) + + // Success + pk, err := verifyRekorFulcio(rekorKeyECDSA, &fulcioTrustRoot{ + caCertificates: caCertificates, + oidcIssuer: "https://github.com/login/oauth", + subjectEmail: "mitr@redhat.com", + }, setBytes, certBytes, chainBytes, string(sigBase64), payloadBytes) + require.NoError(t, err) + assertPublicKeyMatchesCert(t, certBytes, pk) + + // Rekor failure + pk, err = verifyRekorFulcio(rekorKeyECDSA, &fulcioTrustRoot{ + caCertificates: caCertificates, + oidcIssuer: "https://github.com/login/oauth", + subjectEmail: "mitr@redhat.com", + }, setBytes, certBytes, chainBytes, string(sigBase64), []byte("this payload does not match")) + assert.Error(t, err) + assert.Nil(t, pk) + + // Fulcio failure + pk, err = verifyRekorFulcio(rekorKeyECDSA, &fulcioTrustRoot{ + caCertificates: caCertificates, + oidcIssuer: "https://github.com/login/oauth", + subjectEmail: "this-does-not-match@example.com", + }, setBytes, certBytes, chainBytes, string(sigBase64), payloadBytes) + assert.Error(t, err) + assert.Nil(t, pk) +} diff --git a/signature/internal/json.go b/signature/internal/json.go index 0f39fe0ad2..a9d127e656 100644 --- a/signature/internal/json.go +++ b/signature/internal/json.go @@ -5,6 +5,8 @@ import ( "encoding/json" "fmt" "io" + + "github.com/containers/image/v5/internal/set" ) // JSONFormatError is returned when JSON does not match expected format. @@ -20,8 +22,8 @@ func (err JSONFormatError) Error() string { // // The fieldResolver approach is useful for decoding the Policy.Transports map; using it for structs is a bit lazy, // we could use reflection to automate this. Later? -func ParanoidUnmarshalJSONObject(data []byte, fieldResolver func(string) interface{}) error { - seenKeys := map[string]struct{}{} +func ParanoidUnmarshalJSONObject(data []byte, fieldResolver func(string) any) error { + seenKeys := set.New[string]() dec := json.NewDecoder(bytes.NewReader(data)) t, err := dec.Token() @@ -45,10 +47,10 @@ func ParanoidUnmarshalJSONObject(data []byte, fieldResolver func(string) interfa // Coverage: This should never happen, dec.Token() rejects non-string-literals in this state. return JSONFormatError(fmt.Sprintf("Key string literal expected, got \"%s\"", t)) } - if _, ok := seenKeys[key]; ok { + if seenKeys.Contains(key) { return JSONFormatError(fmt.Sprintf("Duplicate key \"%s\"", key)) } - seenKeys[key] = struct{}{} + seenKeys.Add(key) valuePtr := fieldResolver(key) if valuePtr == nil { @@ -68,11 +70,11 @@ func ParanoidUnmarshalJSONObject(data []byte, fieldResolver func(string) interfa // ParanoidUnmarshalJSONObjectExactFields unmarshals data as a JSON object, but failing on the slightest unexpected aspect // (including duplicated keys, unrecognized keys, and non-matching types). Each of the fields in exactFields // must be present exactly once, and none other fields are accepted. -func ParanoidUnmarshalJSONObjectExactFields(data []byte, exactFields map[string]interface{}) error { - seenKeys := map[string]struct{}{} - if err := ParanoidUnmarshalJSONObject(data, func(key string) interface{} { +func ParanoidUnmarshalJSONObjectExactFields(data []byte, exactFields map[string]any) error { + seenKeys := set.New[string]() + if err := ParanoidUnmarshalJSONObject(data, func(key string) any { if valuePtr, ok := exactFields[key]; ok { - seenKeys[key] = struct{}{} + seenKeys.Add(key) return valuePtr } return nil @@ -80,7 +82,7 @@ func ParanoidUnmarshalJSONObjectExactFields(data []byte, exactFields map[string] return err } for key := range exactFields { - if _, ok := seenKeys[key]; !ok { + if !seenKeys.Contains(key) { return JSONFormatError(fmt.Sprintf(`Key "%s" missing in a JSON object`, key)) } } diff --git a/signature/internal/json_test.go b/signature/internal/json_test.go index bb408333fb..f3a6ff0198 100644 --- a/signature/internal/json_test.go +++ b/signature/internal/json_test.go @@ -8,6 +8,8 @@ import ( "github.com/stretchr/testify/require" ) +type mSA map[string]any // To minimize typing the long name + // implementsUnmarshalJSON is a minimalistic type used to detect that // paranoidUnmarshalJSONObject uses the json.Unmarshaler interface of resolved // pointers. @@ -29,7 +31,7 @@ func TestParanoidUnmarshalJSONObject(t *testing.T) { } ts := testStruct{} var unmarshalJSONCalled implementsUnmarshalJSON - tsResolver := func(key string) interface{} { + tsResolver := func(key string) any { switch key { case "a": return &ts.A @@ -85,7 +87,7 @@ func TestParanoidUnmarshalJSONObjectExactFields(t *testing.T) { var float64Value float64 var rawValue json.RawMessage var unmarshallCalled implementsUnmarshalJSON - exactFields := map[string]interface{}{ + exactFields := map[string]any{ "string": &stringValue, "float64": &float64Value, "raw": &rawValue, @@ -93,7 +95,7 @@ func TestParanoidUnmarshalJSONObjectExactFields(t *testing.T) { } // Empty object - err := ParanoidUnmarshalJSONObjectExactFields([]byte(`{}`), map[string]interface{}{}) + err := ParanoidUnmarshalJSONObjectExactFields([]byte(`{}`), map[string]any{}) require.NoError(t, err) // Success @@ -121,3 +123,16 @@ func TestParanoidUnmarshalJSONObjectExactFields(t *testing.T) { assert.Error(t, err, input) } } + +// Return the result of modifying validJSON with fn +func modifiedJSON(t *testing.T, validJSON []byte, modifyFn func(mSA)) []byte { + var tmp mSA + err := json.Unmarshal(validJSON, &tmp) + require.NoError(t, err) + + modifyFn(tmp) + + modifiedJSON, err := json.Marshal(tmp) + require.NoError(t, err) + return modifiedJSON +} diff --git a/signature/internal/rekor_set.go b/signature/internal/rekor_set.go new file mode 100644 index 0000000000..d439b5f7a7 --- /dev/null +++ b/signature/internal/rekor_set.go @@ -0,0 +1,237 @@ +package internal + +import ( + "bytes" + "crypto/ecdsa" + "crypto/sha256" + "encoding/base64" + "encoding/hex" + "encoding/json" + "encoding/pem" + "fmt" + "time" + + "github.com/cyberphone/json-canonicalization/go/src/webpki.org/jsoncanonicalizer" + "github.com/sigstore/rekor/pkg/generated/models" +) + +// This is the github.com/sigstore/rekor/pkg/generated/models.Hashedrekord.APIVersion for github.com/sigstore/rekor/pkg/generated/models.HashedrekordV001Schema. +// We could alternatively use github.com/sigstore/rekor/pkg/types/hashedrekord.APIVERSION, but that subpackage adds too many dependencies. +const HashedRekordV001APIVersion = "0.0.1" + +// UntrustedRekorSET is a parsed content of the sigstore-signature Rekor SET +// (note that this a signature-specific format, not a format directly used by the Rekor API). +// This corresponds to github.com/sigstore/cosign/bundle.RekorBundle, but we impose a stricter decoder. +type UntrustedRekorSET struct { + UntrustedSignedEntryTimestamp []byte // A signature over some canonical JSON form of UntrustedPayload + UntrustedPayload json.RawMessage +} + +type UntrustedRekorPayload struct { + Body []byte // In cosign, this is an any, but only a string works + IntegratedTime int64 + LogIndex int64 + LogID string +} + +// A compile-time check that UntrustedRekorSET implements json.Unmarshaler +var _ json.Unmarshaler = (*UntrustedRekorSET)(nil) + +// UnmarshalJSON implements the json.Unmarshaler interface +func (s *UntrustedRekorSET) UnmarshalJSON(data []byte) error { + err := s.strictUnmarshalJSON(data) + if err != nil { + if formatErr, ok := err.(JSONFormatError); ok { + err = NewInvalidSignatureError(formatErr.Error()) + } + } + return err +} + +// strictUnmarshalJSON is UnmarshalJSON, except that it may return the internal JSONFormatError error type. +// Splitting it into a separate function allows us to do the JSONFormatError → InvalidSignatureError in a single place, the caller. +func (s *UntrustedRekorSET) strictUnmarshalJSON(data []byte) error { + return ParanoidUnmarshalJSONObjectExactFields(data, map[string]any{ + "SignedEntryTimestamp": &s.UntrustedSignedEntryTimestamp, + "Payload": &s.UntrustedPayload, + }) +} + +// A compile-time check that UntrustedRekorSET and *UntrustedRekorSET implements json.Marshaler +var _ json.Marshaler = UntrustedRekorSET{} +var _ json.Marshaler = (*UntrustedRekorSET)(nil) + +// MarshalJSON implements the json.Marshaler interface. +func (s UntrustedRekorSET) MarshalJSON() ([]byte, error) { + return json.Marshal(map[string]any{ + "SignedEntryTimestamp": s.UntrustedSignedEntryTimestamp, + "Payload": s.UntrustedPayload, + }) +} + +// A compile-time check that UntrustedRekorPayload implements json.Unmarshaler +var _ json.Unmarshaler = (*UntrustedRekorPayload)(nil) + +// UnmarshalJSON implements the json.Unmarshaler interface +func (p *UntrustedRekorPayload) UnmarshalJSON(data []byte) error { + err := p.strictUnmarshalJSON(data) + if err != nil { + if formatErr, ok := err.(JSONFormatError); ok { + err = NewInvalidSignatureError(formatErr.Error()) + } + } + return err +} + +// strictUnmarshalJSON is UnmarshalJSON, except that it may return the internal JSONFormatError error type. +// Splitting it into a separate function allows us to do the JSONFormatError → InvalidSignatureError in a single place, the caller. +func (p *UntrustedRekorPayload) strictUnmarshalJSON(data []byte) error { + return ParanoidUnmarshalJSONObjectExactFields(data, map[string]any{ + "body": &p.Body, + "integratedTime": &p.IntegratedTime, + "logIndex": &p.LogIndex, + "logID": &p.LogID, + }) +} + +// A compile-time check that UntrustedRekorPayload and *UntrustedRekorPayload implements json.Marshaler +var _ json.Marshaler = UntrustedRekorPayload{} +var _ json.Marshaler = (*UntrustedRekorPayload)(nil) + +// MarshalJSON implements the json.Marshaler interface. +func (p UntrustedRekorPayload) MarshalJSON() ([]byte, error) { + return json.Marshal(map[string]any{ + "body": p.Body, + "integratedTime": p.IntegratedTime, + "logIndex": p.LogIndex, + "logID": p.LogID, + }) +} + +// VerifyRekorSET verifies that unverifiedRekorSET is correctly signed by publicKey and matches the rest of the data. +// Returns bundle upload time on success. +func VerifyRekorSET(publicKey *ecdsa.PublicKey, unverifiedRekorSET []byte, unverifiedKeyOrCertBytes []byte, unverifiedBase64Signature string, unverifiedPayloadBytes []byte) (time.Time, error) { + // FIXME: Should the publicKey parameter hard-code ecdsa? + + // == Parse SET bytes + var untrustedSET UntrustedRekorSET + // Sadly. we need to parse and transform untrusted data before verifying a cryptographic signature... + if err := json.Unmarshal(unverifiedRekorSET, &untrustedSET); err != nil { + return time.Time{}, NewInvalidSignatureError(err.Error()) + } + // == Verify SET signature + // Cosign unmarshals and re-marshals UntrustedPayload; that seems unnecessary, + // assuming jsoncanonicalizer is designed to operate on untrusted data. + untrustedSETPayloadCanonicalBytes, err := jsoncanonicalizer.Transform(untrustedSET.UntrustedPayload) + if err != nil { + return time.Time{}, NewInvalidSignatureError(fmt.Sprintf("canonicalizing Rekor SET JSON: %v", err)) + } + untrustedSETPayloadHash := sha256.Sum256(untrustedSETPayloadCanonicalBytes) + if !ecdsa.VerifyASN1(publicKey, untrustedSETPayloadHash[:], untrustedSET.UntrustedSignedEntryTimestamp) { + return time.Time{}, NewInvalidSignatureError("cryptographic signature verification of Rekor SET failed") + } + + // == Parse SET payload + // Parse the cryptographically-verified canonicalized variant, NOT the originally-delivered representation, + // to decrease risk of exploiting the JSON parser. Note that if there were an arbitrary execution vulnerability, the attacker + // could have exploited the parsing of unverifiedRekorSET above already; so this, at best, ensures more consistent processing + // of the SET payload. + var rekorPayload UntrustedRekorPayload + if err := json.Unmarshal(untrustedSETPayloadCanonicalBytes, &rekorPayload); err != nil { + return time.Time{}, NewInvalidSignatureError(fmt.Sprintf("parsing Rekor SET payload: %v", err.Error())) + } + // FIXME: Use a different decoder implementation? The Swagger-generated code is kinda ridiculous, with the need to re-marshal + // hashedRekor.Spec and so on. + // Especially if we anticipate needing to decode different data formats… + // That would also allow being much more strict about JSON. + // + // Alternatively, rely on the existing .Validate() methods instead of manually checking for nil all over the place. + var hashedRekord models.Hashedrekord + if err := json.Unmarshal(rekorPayload.Body, &hashedRekord); err != nil { + return time.Time{}, NewInvalidSignatureError(fmt.Sprintf("decoding the body of a Rekor SET payload: %v", err)) + } + // The decode of models.HashedRekord validates the "kind": "hashedrecord" field, which is otherwise invisible to us. + if hashedRekord.APIVersion == nil { + return time.Time{}, NewInvalidSignatureError("missing Rekor SET Payload API version") + } + if *hashedRekord.APIVersion != HashedRekordV001APIVersion { + return time.Time{}, NewInvalidSignatureError(fmt.Sprintf("unsupported Rekor SET Payload hashedrekord version %#v", hashedRekord.APIVersion)) + } + hashedRekordV001Bytes, err := json.Marshal(hashedRekord.Spec) + if err != nil { + // Coverage: hashedRekord.Spec is an any that was just unmarshaled, + // so this should never fail. + return time.Time{}, NewInvalidSignatureError(fmt.Sprintf("re-creating hashedrekord spec: %v", err)) + } + var hashedRekordV001 models.HashedrekordV001Schema + if err := json.Unmarshal(hashedRekordV001Bytes, &hashedRekordV001); err != nil { + return time.Time{}, NewInvalidSignatureError(fmt.Sprintf("decoding hashedrekod spec: %v", err)) + } + + // == Match unverifiedKeyOrCertBytes + if hashedRekordV001.Signature == nil { + return time.Time{}, NewInvalidSignatureError(`Missing "signature" field in hashedrekord`) + } + if hashedRekordV001.Signature.PublicKey == nil { + return time.Time{}, NewInvalidSignatureError(`Missing "signature.publicKey" field in hashedrekord`) + + } + rekorKeyOrCertPEM, rest := pem.Decode(hashedRekordV001.Signature.PublicKey.Content) + if rekorKeyOrCertPEM == nil { + return time.Time{}, NewInvalidSignatureError("publicKey in Rekor SET is not in PEM format") + } + if len(rest) != 0 { + return time.Time{}, NewInvalidSignatureError("publicKey in Rekor SET has trailing data") + } + // FIXME: For public keys, let the caller provide the DER-formatted blob instead + // of round-tripping through PEM. + unverifiedKeyOrCertPEM, rest := pem.Decode(unverifiedKeyOrCertBytes) + if unverifiedKeyOrCertPEM == nil { + return time.Time{}, NewInvalidSignatureError("public key or cert to be matched against publicKey in Rekor SET is not in PEM format") + } + if len(rest) != 0 { + return time.Time{}, NewInvalidSignatureError("public key or cert to be matched against publicKey in Rekor SET has trailing data") + } + // NOTE: This compares the PEM payload, but not the object type or headers. + if !bytes.Equal(rekorKeyOrCertPEM.Bytes, unverifiedKeyOrCertPEM.Bytes) { + return time.Time{}, NewInvalidSignatureError("publicKey in Rekor SET does not match") + } + // == Match unverifiedSignatureBytes + unverifiedSignatureBytes, err := base64.StdEncoding.DecodeString(unverifiedBase64Signature) + if err != nil { + return time.Time{}, NewInvalidSignatureError(fmt.Sprintf("decoding signature base64: %v", err)) + } + if !bytes.Equal(hashedRekordV001.Signature.Content, unverifiedSignatureBytes) { + return time.Time{}, NewInvalidSignatureError(fmt.Sprintf("signature in Rekor SET does not match: %#v vs. %#v", + string(hashedRekordV001.Signature.Content), string(unverifiedSignatureBytes))) + } + + // == Match unverifiedPayloadBytes + if hashedRekordV001.Data == nil { + return time.Time{}, NewInvalidSignatureError(`Missing "data" field in hashedrekord`) + } + if hashedRekordV001.Data.Hash == nil { + return time.Time{}, NewInvalidSignatureError(`Missing "data.hash" field in hashedrekord`) + } + if hashedRekordV001.Data.Hash.Algorithm == nil { + return time.Time{}, NewInvalidSignatureError(`Missing "data.hash.algorithm" field in hashedrekord`) + } + if *hashedRekordV001.Data.Hash.Algorithm != models.HashedrekordV001SchemaDataHashAlgorithmSha256 { + return time.Time{}, NewInvalidSignatureError(fmt.Sprintf(`Unexpected "data.hash.algorithm" value %#v`, *hashedRekordV001.Data.Hash.Algorithm)) + } + if hashedRekordV001.Data.Hash.Value == nil { + return time.Time{}, NewInvalidSignatureError(`Missing "data.hash.value" field in hashedrekord`) + } + rekorPayloadHash, err := hex.DecodeString(*hashedRekordV001.Data.Hash.Value) + if err != nil { + return time.Time{}, NewInvalidSignatureError(fmt.Sprintf(`Invalid "data.hash.value" field in hashedrekord: %v`, err)) + + } + unverifiedPayloadHash := sha256.Sum256(unverifiedPayloadBytes) + if !bytes.Equal(rekorPayloadHash, unverifiedPayloadHash[:]) { + return time.Time{}, NewInvalidSignatureError("payload in Rekor SET does not match") + } + + // == All OK; return the relevant time. + return time.Unix(rekorPayload.IntegratedTime, 0), nil +} diff --git a/signature/internal/rekor_set_test.go b/signature/internal/rekor_set_test.go new file mode 100644 index 0000000000..0cc8483d4a --- /dev/null +++ b/signature/internal/rekor_set_test.go @@ -0,0 +1,400 @@ +package internal + +import ( + "bytes" + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/sha256" + "crypto/sha512" + "encoding/base64" + "encoding/hex" + "encoding/json" + "os" + "testing" + "time" + + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" + "github.com/sigstore/rekor/pkg/generated/models" + "github.com/sigstore/sigstore/pkg/cryptoutils" + sigstoreSignature "github.com/sigstore/sigstore/pkg/signature" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// Verify that input can be unmarshaled as an UntrustedRekorSET. +func successfullyUnmarshalUntrustedRekorSET(t *testing.T, input []byte) UntrustedRekorSET { + var s UntrustedRekorSET + err := json.Unmarshal(input, &s) + require.NoError(t, err, string(input)) + + return s +} + +// Verify that input can't be unmarshaled as an UntrustedRekorSET. +func assertUnmarshalUntrustedRekorSETFails(t *testing.T, input []byte) { + var s UntrustedRekorSET + err := json.Unmarshal(input, &s) + assert.Error(t, err, string(input)) +} + +func TestUntrustedRekorSETUnmarshalJSON(t *testing.T) { + // Invalid input. Note that json.Unmarshal is guaranteed to validate input before calling our + // UnmarshalJSON implementation; so test that first, then test our error handling for completeness. + assertUnmarshalUntrustedRekorSETFails(t, []byte("&")) + var s UntrustedRekorSET + err := s.UnmarshalJSON([]byte("&")) + assert.Error(t, err) + + // Not an object + assertUnmarshalUntrustedRekorSETFails(t, []byte("1")) + + // Start with a valid JSON. + validSET := UntrustedRekorSET{ + UntrustedSignedEntryTimestamp: []byte("signedTimestamp#@!"), + UntrustedPayload: json.RawMessage(`["payload#@!"]`), + } + validJSON, err := json.Marshal(validSET) + require.NoError(t, err) + + // Success + s = successfullyUnmarshalUntrustedRekorSET(t, validJSON) + assert.Equal(t, validSET, s) + + // A /usr/bin/cosign-generated payload is handled correctly + setBytes, err := os.ReadFile("testdata/rekor-set") + require.NoError(t, err) + s = successfullyUnmarshalUntrustedRekorSET(t, setBytes) + expectedSET, err := base64.StdEncoding.DecodeString(`MEYCIQDdeujdGLpMTgFdew9wsSJ3WF7olX9PawgzGeX2RmJd8QIhAPxGJf+HjUFVpQc0hgPaUSK8LsONJ08fZFEBVKDeLj4S`) + require.NoError(t, err) + assert.Equal(t, UntrustedRekorSET{ + UntrustedSignedEntryTimestamp: expectedSET, + UntrustedPayload: []byte(`{"body":"eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiIwZmQxZTk4MzJjYzVhNWY1MDJlODAwZmU5Y2RlZWZiZDMxMzYyZGYxNmZlOGMyMjUwZDMwOGFlYTNmYjFmYzY5In19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FUUNJRUpjOTZlMDQxVkFoS0EwM1N2ZkNZYldvZElNSVFQeUF0V3lEUDRGblBxcEFpQWFJUzYwRWpoUkRoU2Fub0Zzb0l5OGZLcXFLZVc1cHMvdExYU0dwYXlpMmc9PSIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVTnVWRU5EUVdsUFowRjNTVUpCWjBsVlJ6UTFkV0ZETW5vNFZuWjFUM2Q2ZW0wM09WSlFabU5yYjNoM2QwTm5XVWxMYjFwSmVtb3dSVUYzVFhjS1RucEZWazFDVFVkQk1WVkZRMmhOVFdNeWJHNWpNMUoyWTIxVmRWcEhWakpOVWpSM1NFRlpSRlpSVVVSRmVGWjZZVmRrZW1SSE9YbGFVekZ3WW01U2JBcGpiVEZzV2tkc2FHUkhWWGRJYUdOT1RXcEplRTFxUlhsTlZHY3dUMFJGTkZkb1kwNU5ha2w0VFdwRmVVMVVaekZQUkVVMFYycEJRVTFHYTNkRmQxbElDa3R2V2tsNmFqQkRRVkZaU1V0dldrbDZhakJFUVZGalJGRm5RVVZLUW5WeWEyaFRVbTFJZFd0SVZtZ3pWa0U0YmxsMVUxZHhXalJzZEdGbVJIZDVWMndLWXpOak9VNWhURzkyYlhVclRrTTBUbWxWZEUxTWFXWk1MMUF6Ym5GbFpHSnVZM1JMUW5WWmJXWkpVMGRwV214V2VVdFBRMEZWU1hkblowVXJUVUUwUndwQk1WVmtSSGRGUWk5M1VVVkJkMGxJWjBSQlZFSm5UbFpJVTFWRlJFUkJTMEpuWjNKQ1owVkdRbEZqUkVGNlFXUkNaMDVXU0ZFMFJVWm5VVlZCYlhaSENqUkxObXhPYXk5emF5OW1OR0ZwWVdocVdWSnhVaXRqZDBoM1dVUldVakJxUWtKbmQwWnZRVlV6T1ZCd2VqRlphMFZhWWpWeFRtcHdTMFpYYVhocE5Ga0tXa1E0ZDBoUldVUldVakJTUVZGSUwwSkNUWGRGV1VWUVlsZHNNR05yUW5sYVYxSnZXVmhSZFZreU9YUk5RM2RIUTJselIwRlJVVUpuTnpoM1FWRkZSUXBJYldnd1pFaENlazlwT0haYU1td3dZVWhXYVV4dFRuWmlVemx6WWpKa2NHSnBPWFpaV0ZZd1lVUkRRbWxSV1V0TGQxbENRa0ZJVjJWUlNVVkJaMUkzQ2tKSWEwRmtkMEl4UVU0d09VMUhja2Q0ZUVWNVdYaHJaVWhLYkc1T2QwdHBVMncyTkROcWVYUXZOR1ZMWTI5QmRrdGxOazlCUVVGQ2FGRmxjV3gzVlVFS1FVRlJSRUZGV1hkU1FVbG5WbWt5VTNaT05WSmxSMWxwVDFOb1dUaE1SbE5TUnpWRU5tOUJWRXR4U0dJMmEwNHZSRXBvTW5KQlZVTkpRVTVUTmtGeGNBcDRZVmhwU0hkVVNGVnlNM2hRVTBkaE5XazJhSGwzYldKaVVrTTJUakJyU1dWRVRUWk5RVzlIUTBOeFIxTk5ORGxDUVUxRVFUSm5RVTFIVlVOTlEweDFDbU5hZEVWVFNVNHdiRzAyTkVOdkwySmFOamhEUTFKclYyeHJkRmcwYlcxS2FWSm9TMms1WXpsUlJEWXlRelZUZFZwb1l6QjJkbTgyVFU5TGJWUlJTWGdLUVVsdkwwMXZlbHBsYjFVM2NtUk9hakJ3V2t0MVFtVkRiVTF4YlVwaFJGTnpkekU1ZEV0cEwySXhjRVZ0ZFhjclUyWXlRa2t5TlVkblNXSkxlblJITVFvNWR6MDlDaTB0TFMwdFJVNUVJRU5GVWxSSlJrbERRVlJGTFMwdExTMEsifX19fQ==","integratedTime":1670870899,"logIndex":8949589,"logID":"c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d"}`), + }, s) + + // Various ways to corrupt the JSON + breakFns := []func(mSA){ + // A top-level field is missing + func(v mSA) { delete(v, "SignedEntryTimestamp") }, + func(v mSA) { delete(v, "Payload") }, + // Extra top-level sub-object + func(v mSA) { v["unexpected"] = 1 }, + // "SignedEntryTimestamp" not a string + func(v mSA) { v["critical"] = 1 }, + // "Payload" not an object + func(v mSA) { v["optional"] = 1 }, + } + for _, fn := range breakFns { + testJSON := modifiedJSON(t, validJSON, fn) + assertUnmarshalUntrustedRekorSETFails(t, testJSON) + } +} + +// Verify that input can be unmarshaled as an UntrustedRekorPayload. +func successfullyUnmarshalUntrustedRekorPayload(t *testing.T, input []byte) UntrustedRekorPayload { + var s UntrustedRekorPayload + err := json.Unmarshal(input, &s) + require.NoError(t, err, string(input)) + + return s +} + +// Verify that input can't be unmarshaled as an UntrustedRekorPayload. +func assertUnmarshalUntrustedRekorPayloadFails(t *testing.T, input []byte) { + var s UntrustedRekorPayload + err := json.Unmarshal(input, &s) + assert.Error(t, err, string(input)) +} + +func TestUntrustedRekorPayloadUnmarshalJSON(t *testing.T) { + // Invalid input. Note that json.Unmarshal is guaranteed to validate input before calling our + // UnmarshalJSON implementation; so test that first, then test our error handling for completeness. + assertUnmarshalUntrustedRekorPayloadFails(t, []byte("&")) + var p UntrustedRekorPayload + err := p.UnmarshalJSON([]byte("&")) + assert.Error(t, err) + + // Not an object + assertUnmarshalUntrustedRekorPayloadFails(t, []byte("1")) + + // Start with a valid JSON. + validPayload := UntrustedRekorPayload{ + Body: []byte(`["json"]`), + IntegratedTime: 1, + LogIndex: 2, + LogID: "abc", + } + validJSON, err := validPayload.MarshalJSON() + require.NoError(t, err) + + // Success + p = successfullyUnmarshalUntrustedRekorPayload(t, validJSON) + assert.Equal(t, validPayload, p) + + // A /usr/bin/cosign-generated payload is handled correctly + setBytes, err := os.ReadFile("testdata/rekor-set") + require.NoError(t, err) + s := successfullyUnmarshalUntrustedRekorSET(t, setBytes) + p = successfullyUnmarshalUntrustedRekorPayload(t, s.UntrustedPayload) + expectedBody, err := base64.StdEncoding.DecodeString(`eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiIwZmQxZTk4MzJjYzVhNWY1MDJlODAwZmU5Y2RlZWZiZDMxMzYyZGYxNmZlOGMyMjUwZDMwOGFlYTNmYjFmYzY5In19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FUUNJRUpjOTZlMDQxVkFoS0EwM1N2ZkNZYldvZElNSVFQeUF0V3lEUDRGblBxcEFpQWFJUzYwRWpoUkRoU2Fub0Zzb0l5OGZLcXFLZVc1cHMvdExYU0dwYXlpMmc9PSIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVTnVWRU5EUVdsUFowRjNTVUpCWjBsVlJ6UTFkV0ZETW5vNFZuWjFUM2Q2ZW0wM09WSlFabU5yYjNoM2QwTm5XVWxMYjFwSmVtb3dSVUYzVFhjS1RucEZWazFDVFVkQk1WVkZRMmhOVFdNeWJHNWpNMUoyWTIxVmRWcEhWakpOVWpSM1NFRlpSRlpSVVVSRmVGWjZZVmRrZW1SSE9YbGFVekZ3WW01U2JBcGpiVEZzV2tkc2FHUkhWWGRJYUdOT1RXcEplRTFxUlhsTlZHY3dUMFJGTkZkb1kwNU5ha2w0VFdwRmVVMVVaekZQUkVVMFYycEJRVTFHYTNkRmQxbElDa3R2V2tsNmFqQkRRVkZaU1V0dldrbDZhakJFUVZGalJGRm5RVVZLUW5WeWEyaFRVbTFJZFd0SVZtZ3pWa0U0YmxsMVUxZHhXalJzZEdGbVJIZDVWMndLWXpOak9VNWhURzkyYlhVclRrTTBUbWxWZEUxTWFXWk1MMUF6Ym5GbFpHSnVZM1JMUW5WWmJXWkpVMGRwV214V2VVdFBRMEZWU1hkblowVXJUVUUwUndwQk1WVmtSSGRGUWk5M1VVVkJkMGxJWjBSQlZFSm5UbFpJVTFWRlJFUkJTMEpuWjNKQ1owVkdRbEZqUkVGNlFXUkNaMDVXU0ZFMFJVWm5VVlZCYlhaSENqUkxObXhPYXk5emF5OW1OR0ZwWVdocVdWSnhVaXRqZDBoM1dVUldVakJxUWtKbmQwWnZRVlV6T1ZCd2VqRlphMFZhWWpWeFRtcHdTMFpYYVhocE5Ga0tXa1E0ZDBoUldVUldVakJTUVZGSUwwSkNUWGRGV1VWUVlsZHNNR05yUW5sYVYxSnZXVmhSZFZreU9YUk5RM2RIUTJselIwRlJVVUpuTnpoM1FWRkZSUXBJYldnd1pFaENlazlwT0haYU1td3dZVWhXYVV4dFRuWmlVemx6WWpKa2NHSnBPWFpaV0ZZd1lVUkRRbWxSV1V0TGQxbENRa0ZJVjJWUlNVVkJaMUkzQ2tKSWEwRmtkMEl4UVU0d09VMUhja2Q0ZUVWNVdYaHJaVWhLYkc1T2QwdHBVMncyTkROcWVYUXZOR1ZMWTI5QmRrdGxOazlCUVVGQ2FGRmxjV3gzVlVFS1FVRlJSRUZGV1hkU1FVbG5WbWt5VTNaT05WSmxSMWxwVDFOb1dUaE1SbE5TUnpWRU5tOUJWRXR4U0dJMmEwNHZSRXBvTW5KQlZVTkpRVTVUTmtGeGNBcDRZVmhwU0hkVVNGVnlNM2hRVTBkaE5XazJhSGwzYldKaVVrTTJUakJyU1dWRVRUWk5RVzlIUTBOeFIxTk5ORGxDUVUxRVFUSm5RVTFIVlVOTlEweDFDbU5hZEVWVFNVNHdiRzAyTkVOdkwySmFOamhEUTFKclYyeHJkRmcwYlcxS2FWSm9TMms1WXpsUlJEWXlRelZUZFZwb1l6QjJkbTgyVFU5TGJWUlJTWGdLUVVsdkwwMXZlbHBsYjFVM2NtUk9hakJ3V2t0MVFtVkRiVTF4YlVwaFJGTnpkekU1ZEV0cEwySXhjRVZ0ZFhjclUyWXlRa2t5TlVkblNXSkxlblJITVFvNWR6MDlDaTB0TFMwdFJVNUVJRU5GVWxSSlJrbERRVlJGTFMwdExTMEsifX19fQ==`) + require.NoError(t, err) + assert.Equal(t, UntrustedRekorPayload{ + Body: expectedBody, + IntegratedTime: 1670870899, + LogIndex: 8949589, + LogID: "c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d", + }, p) + + // Various ways to corrupt the JSON + breakFns := []func(mSA){ + // A top-level field is missing + func(v mSA) { delete(v, "body") }, + func(v mSA) { delete(v, "integratedTime") }, + func(v mSA) { delete(v, "logIndex") }, + func(v mSA) { delete(v, "logID") }, + // Extra top-level sub-object + func(v mSA) { v["unexpected"] = 1 }, + // "body" not a string + func(v mSA) { v["body"] = 1 }, + // "integratedTime" not an integer + func(v mSA) { v["integratedTime"] = "hello" }, + // "logIndex" not an integer + func(v mSA) { v["logIndex"] = "hello" }, + // "logID" not a string + func(v mSA) { v["logID"] = 1 }, + } + for _, fn := range breakFns { + testJSON := modifiedJSON(t, validJSON, fn) + assertUnmarshalUntrustedRekorPayloadFails(t, testJSON) + } +} + +func TestVerifyRekorSET(t *testing.T) { + cosignRekorKeyPEM, err := os.ReadFile("testdata/rekor.pub") + require.NoError(t, err) + cosignRekorKey, err := cryptoutils.UnmarshalPEMToPublicKey(cosignRekorKeyPEM) + require.NoError(t, err) + cosignRekorKeyECDSA, ok := cosignRekorKey.(*ecdsa.PublicKey) + require.True(t, ok) + cosignSETBytes, err := os.ReadFile("testdata/rekor-set") + require.NoError(t, err) + cosignCertBytes, err := os.ReadFile("testdata/rekor-cert") + require.NoError(t, err) + cosignSigBase64, err := os.ReadFile("testdata/rekor-sig") + require.NoError(t, err) + cosignPayloadBytes, err := os.ReadFile("testdata/rekor-payload") + require.NoError(t, err) + + // Successful verification + tm, err := VerifyRekorSET(cosignRekorKeyECDSA, cosignSETBytes, cosignCertBytes, string(cosignSigBase64), cosignPayloadBytes) + require.NoError(t, err) + assert.Equal(t, time.Unix(1670870899, 0), tm) + + // For extra paranoia, test that we return a zero time on error. + + // A completely invalid SET. + tm, err = VerifyRekorSET(cosignRekorKeyECDSA, []byte{}, cosignCertBytes, string(cosignSigBase64), cosignPayloadBytes) + assert.Error(t, err) + assert.Zero(t, tm) + + tm, err = VerifyRekorSET(cosignRekorKeyECDSA, []byte("invalid signature"), cosignCertBytes, string(cosignSigBase64), cosignPayloadBytes) + assert.Error(t, err) + assert.Zero(t, tm) + + testKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + testSigner, err := sigstoreSignature.LoadECDSASigner(testKey, crypto.SHA256) + require.NoError(t, err) + + // JSON canonicalization fails: + // This payload is invalid because it has duplicate fields. + // Right now, that particular failure (unlike more blatantly invalid JSON) is allowed + // by json.Marshal, but detected by jsoncanonicalizer.Transform. + invalidPayload := []byte(`{"logIndex":1, "integratedTime":2,"body":"abc","logID":"def","body":"ABC"}`) + invalidPayloadSig, err := testSigner.SignMessage(bytes.NewReader(invalidPayload)) + require.NoError(t, err) + invalidSET, err := json.Marshal(UntrustedRekorSET{ + UntrustedSignedEntryTimestamp: invalidPayloadSig, + UntrustedPayload: json.RawMessage(invalidPayload), + }) + require.NoError(t, err) + tm, err = VerifyRekorSET(&testKey.PublicKey, invalidSET, cosignCertBytes, string(cosignSigBase64), cosignPayloadBytes) + assert.Error(t, err) + assert.Zero(t, tm) + + // Cryptographic verification fails (a mismatched public key) + tm, err = VerifyRekorSET(&testKey.PublicKey, cosignSETBytes, cosignCertBytes, string(cosignSigBase64), cosignPayloadBytes) + assert.Error(t, err) + assert.Zero(t, tm) + + // Parsing UntrustedRekorPayload fails + invalidPayload = []byte(`{}`) + invalidPayloadSig, err = testSigner.SignMessage(bytes.NewReader(invalidPayload)) + require.NoError(t, err) + invalidSET, err = json.Marshal(UntrustedRekorSET{ + UntrustedSignedEntryTimestamp: invalidPayloadSig, + UntrustedPayload: json.RawMessage(invalidPayload), + }) + require.NoError(t, err) + tm, err = VerifyRekorSET(&testKey.PublicKey, invalidSET, cosignCertBytes, string(cosignSigBase64), cosignPayloadBytes) + assert.Error(t, err) + assert.Zero(t, tm) + + // A correctly signed UntrustedRekorPayload is invalid + cosignPayloadSHA256 := sha256.Sum256(cosignPayloadBytes) + cosignSigBytes, err := base64.StdEncoding.DecodeString(string(cosignSigBase64)) + require.NoError(t, err) + validHashedRekord := models.Hashedrekord{ + APIVersion: swag.String(HashedRekordV001APIVersion), + Spec: models.HashedrekordV001Schema{ + Data: &models.HashedrekordV001SchemaData{ + Hash: &models.HashedrekordV001SchemaDataHash{ + Algorithm: swag.String(models.HashedrekordV001SchemaDataHashAlgorithmSha256), + Value: swag.String(hex.EncodeToString(cosignPayloadSHA256[:])), + }, + }, + Signature: &models.HashedrekordV001SchemaSignature{ + Content: strfmt.Base64(cosignSigBytes), + PublicKey: &models.HashedrekordV001SchemaSignaturePublicKey{ + Content: strfmt.Base64(cosignCertBytes), + }, + }, + }, + } + validHashedRekordJSON, err := json.Marshal(validHashedRekord) + require.NoError(t, err) + for _, fn := range []func(mSA){ + // A Hashedrekord field is missing + func(v mSA) { delete(v, "apiVersion") }, + func(v mSA) { delete(v, "kind") }, // "kind" is not visible in the type definition, but required by the implementation + func(v mSA) { delete(v, "spec") }, + // This, along with many other extra fields, is currently accepted. That is NOT an API commitment. + // func(v mSA) { v["unexpected"] = 1 }, // Extra top-level field: + // Invalid apiVersion + func(v mSA) { v["apiVersion"] = nil }, + func(v mSA) { v["apiVersion"] = 1 }, + func(v mSA) { v["apiVersion"] = mSA{} }, + func(v mSA) { v["apiVersion"] = "99.0.99" }, + // Invalid kind + func(v mSA) { v["kind"] = nil }, + func(v mSA) { v["kind"] = 1 }, + func(v mSA) { v["kind"] = "notHashedRekord" }, + // Invalid spec + func(v mSA) { v["spec"] = nil }, + func(v mSA) { v["spec"] = 1 }, + // A HashedRekordV001Schema field is missing + func(v mSA) { delete(x(v, "spec"), "data") }, + func(v mSA) { delete(x(v, "spec"), "signature") }, + // Invalid spec.data + func(v mSA) { x(v, "spec")["data"] = nil }, + func(v mSA) { x(v, "spec")["data"] = 1 }, + // Missing spec.data.hash + func(v mSA) { delete(x(v, "spec", "data"), "hash") }, + // Invalid spec.data.hash + func(v mSA) { x(v, "spec", "data")["hash"] = nil }, + func(v mSA) { x(v, "spec", "data")["hash"] = 1 }, + // A spec.data.hash field is missing + func(v mSA) { delete(x(v, "spec", "data", "hash"), "algorithm") }, + func(v mSA) { delete(x(v, "spec", "data", "hash"), "value") }, + // Invalid spec.data.hash.algorithm + func(v mSA) { x(v, "spec", "data", "hash")["algorithm"] = nil }, + func(v mSA) { x(v, "spec", "data", "hash")["algorithm"] = 1 }, + // Invalid spec.data.hash.value + func(v mSA) { x(v, "spec", "data", "hash")["value"] = nil }, + func(v mSA) { x(v, "spec", "data", "hash")["value"] = 1 }, + func(v mSA) { // An odd number of hexadecimal digits + x(v, "spec", "data", "hash")["value"] = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + // spec.data.hash does not match + func(v mSA) { + x(v, "spec", "data", "hash")["value"] = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + // A non-sha256 hash + func(v mSA) { + x(v, "spec", "data", "hash")["algorithm"] = "sha512" + h := sha512.Sum512(cosignPayloadBytes) + x(v, "spec", "data", "hash")["value"] = hex.EncodeToString(h[:]) + }, + // Invalid spec.signature + func(v mSA) { x(v, "spec")["signature"] = nil }, + func(v mSA) { x(v, "spec")["signature"] = 1 }, + // A spec.signature field is missing + func(v mSA) { delete(x(v, "spec", "signature"), "content") }, + func(v mSA) { delete(x(v, "spec", "signature"), "publicKey") }, + // Invalid spec.signature.content + func(v mSA) { x(v, "spec", "signature")["content"] = nil }, + func(v mSA) { x(v, "spec", "signature")["content"] = 1 }, + func(v mSA) { x(v, "spec", "signature")["content"] = "" }, + func(v mSA) { x(v, "spec", "signature")["content"] = "+" }, // Invalid base64 + // spec.signature.content does not match + func(v mSA) { + x(v, "spec", "signature")["content"] = base64.StdEncoding.EncodeToString([]byte("does not match")) + }, + // Invalid spec.signature.publicKey + func(v mSA) { x(v, "spec", "signature")["publicKey"] = nil }, + func(v mSA) { x(v, "spec", "signature")["publicKey"] = 1 }, + // Missing spec.signature.publicKey.content + func(v mSA) { delete(x(v, "spec", "signature", "publicKey"), "content") }, + // Invalid spec.signature.publicKey.content + func(v mSA) { x(v, "spec", "signature", "publicKey")["content"] = nil }, + func(v mSA) { x(v, "spec", "signature", "publicKey")["content"] = 1 }, + func(v mSA) { x(v, "spec", "signature", "publicKey")["content"] = "" }, + func(v mSA) { x(v, "spec", "signature", "publicKey")["content"] = "+" }, // Invalid base64 + func(v mSA) { + x(v, "spec", "signature", "publicKey")["content"] = base64.StdEncoding.EncodeToString([]byte("not PEM")) + }, + func(v mSA) { // Multiple PEM blocks + x(v, "spec", "signature", "publicKey")["content"] = base64.StdEncoding.EncodeToString(bytes.Repeat(cosignCertBytes, 2)) + }, + // spec.signature.publicKey.content does not match + func(v mSA) { + otherKey, err := testSigner.PublicKey() + require.NoError(t, err) + otherPEM, err := cryptoutils.MarshalPublicKeyToPEM(otherKey) + require.NoError(t, err) + x(v, "spec", "signature", "publicKey")["content"] = base64.StdEncoding.EncodeToString(otherPEM) + }, + } { + testHashedRekordJSON := modifiedJSON(t, validHashedRekordJSON, fn) + testPayload, err := json.Marshal(UntrustedRekorPayload{ + Body: testHashedRekordJSON, + IntegratedTime: 1, + LogIndex: 2, + LogID: "logID", + }) + require.NoError(t, err) + testPayloadSig, err := testSigner.SignMessage(bytes.NewReader(testPayload)) + require.NoError(t, err) + testSET, err := json.Marshal(UntrustedRekorSET{ + UntrustedSignedEntryTimestamp: testPayloadSig, + UntrustedPayload: json.RawMessage(testPayload), + }) + require.NoError(t, err) + tm, err = VerifyRekorSET(&testKey.PublicKey, testSET, cosignCertBytes, string(cosignSigBase64), cosignPayloadBytes) + assert.Error(t, err) + assert.Zero(t, tm) + } + + // Invalid unverifiedBase64Signature parameter + truncatedBase64 := cosignSigBase64 + truncatedBase64 = truncatedBase64[:len(truncatedBase64)-1] + tm, err = VerifyRekorSET(cosignRekorKeyECDSA, cosignSETBytes, cosignCertBytes, + string(truncatedBase64), cosignPayloadBytes) + assert.Error(t, err) + assert.Zero(t, tm) + + // Invalid unverifiedKeyOrCertBytes + for _, c := range [][]byte{ + nil, + {}, + []byte("this is not PEM"), + bytes.Repeat(cosignCertBytes, 2), + } { + tm, err = VerifyRekorSET(cosignRekorKeyECDSA, cosignSETBytes, c, + string(cosignSigBase64), cosignPayloadBytes) + assert.Error(t, err) + assert.Zero(t, tm) + } +} diff --git a/signature/internal/sigstore_payload.go b/signature/internal/sigstore_payload.go index bb5e9139d7..f8ec66564d 100644 --- a/signature/internal/sigstore_payload.go +++ b/signature/internal/sigstore_payload.go @@ -21,14 +21,14 @@ const ( // UntrustedSigstorePayload is a parsed content of a sigstore signature payload (not the full signature) type UntrustedSigstorePayload struct { - UntrustedDockerManifestDigest digest.Digest - UntrustedDockerReference string // FIXME: more precise type? - UntrustedCreatorID *string + untrustedDockerManifestDigest digest.Digest + untrustedDockerReference string // FIXME: more precise type? + untrustedCreatorID *string // This is intentionally an int64; the native JSON float64 type would allow to represent _some_ sub-second precision, // but not nearly enough (with current timestamp values, a single unit in the last place is on the order of hundreds of nanoseconds). // So, this is explicitly an int64, and we reject fractional values. If we did need more precise timestamps eventually, // we would add another field, UntrustedTimestampNS int64. - UntrustedTimestamp *int64 + untrustedTimestamp *int64 } // NewUntrustedSigstorePayload returns an UntrustedSigstorePayload object with @@ -39,34 +39,35 @@ func NewUntrustedSigstorePayload(dockerManifestDigest digest.Digest, dockerRefer creatorID := "containers/image " + version.Version timestamp := time.Now().Unix() return UntrustedSigstorePayload{ - UntrustedDockerManifestDigest: dockerManifestDigest, - UntrustedDockerReference: dockerReference, - UntrustedCreatorID: &creatorID, - UntrustedTimestamp: ×tamp, + untrustedDockerManifestDigest: dockerManifestDigest, + untrustedDockerReference: dockerReference, + untrustedCreatorID: &creatorID, + untrustedTimestamp: ×tamp, } } -// Compile-time check that UntrustedSigstorePayload implements json.Marshaler +// A compile-time check that UntrustedSigstorePayload and *UntrustedSigstorePayload implements json.Marshaler +var _ json.Marshaler = UntrustedSigstorePayload{} var _ json.Marshaler = (*UntrustedSigstorePayload)(nil) // MarshalJSON implements the json.Marshaler interface. func (s UntrustedSigstorePayload) MarshalJSON() ([]byte, error) { - if s.UntrustedDockerManifestDigest == "" || s.UntrustedDockerReference == "" { + if s.untrustedDockerManifestDigest == "" || s.untrustedDockerReference == "" { return nil, errors.New("Unexpected empty signature content") } - critical := map[string]interface{}{ + critical := map[string]any{ "type": sigstoreSignatureType, - "image": map[string]string{"docker-manifest-digest": s.UntrustedDockerManifestDigest.String()}, - "identity": map[string]string{"docker-reference": s.UntrustedDockerReference}, + "image": map[string]string{"docker-manifest-digest": s.untrustedDockerManifestDigest.String()}, + "identity": map[string]string{"docker-reference": s.untrustedDockerReference}, } - optional := map[string]interface{}{} - if s.UntrustedCreatorID != nil { - optional["creator"] = *s.UntrustedCreatorID + optional := map[string]any{} + if s.untrustedCreatorID != nil { + optional["creator"] = *s.untrustedCreatorID } - if s.UntrustedTimestamp != nil { - optional["timestamp"] = *s.UntrustedTimestamp + if s.untrustedTimestamp != nil { + optional["timestamp"] = *s.untrustedTimestamp } - signature := map[string]interface{}{ + signature := map[string]any{ "critical": critical, "optional": optional, } @@ -91,7 +92,7 @@ func (s *UntrustedSigstorePayload) UnmarshalJSON(data []byte) error { // Splitting it into a separate function allows us to do the JSONFormatError → InvalidSignatureError in a single place, the caller. func (s *UntrustedSigstorePayload) strictUnmarshalJSON(data []byte) error { var critical, optional json.RawMessage - if err := ParanoidUnmarshalJSONObjectExactFields(data, map[string]interface{}{ + if err := ParanoidUnmarshalJSONObjectExactFields(data, map[string]any{ "critical": &critical, "optional": &optional, }); err != nil { @@ -103,7 +104,7 @@ func (s *UntrustedSigstorePayload) strictUnmarshalJSON(data []byte) error { var gotCreatorID, gotTimestamp = false, false // /usr/bin/cosign generates "optional": null if there are no user-specified annotations. if !bytes.Equal(optional, []byte("null")) { - if err := ParanoidUnmarshalJSONObject(optional, func(key string) interface{} { + if err := ParanoidUnmarshalJSONObject(optional, func(key string) any { switch key { case "creator": gotCreatorID = true @@ -112,7 +113,7 @@ func (s *UntrustedSigstorePayload) strictUnmarshalJSON(data []byte) error { gotTimestamp = true return ×tamp default: - var ignore interface{} + var ignore any return &ignore } }); err != nil { @@ -120,19 +121,19 @@ func (s *UntrustedSigstorePayload) strictUnmarshalJSON(data []byte) error { } } if gotCreatorID { - s.UntrustedCreatorID = &creatorID + s.untrustedCreatorID = &creatorID } if gotTimestamp { intTimestamp := int64(timestamp) if float64(intTimestamp) != timestamp { return NewInvalidSignatureError("Field optional.timestamp is not is not an integer") } - s.UntrustedTimestamp = &intTimestamp + s.untrustedTimestamp = &intTimestamp } var t string var image, identity json.RawMessage - if err := ParanoidUnmarshalJSONObjectExactFields(critical, map[string]interface{}{ + if err := ParanoidUnmarshalJSONObjectExactFields(critical, map[string]any{ "type": &t, "image": &image, "identity": &identity, @@ -144,15 +145,15 @@ func (s *UntrustedSigstorePayload) strictUnmarshalJSON(data []byte) error { } var digestString string - if err := ParanoidUnmarshalJSONObjectExactFields(image, map[string]interface{}{ + if err := ParanoidUnmarshalJSONObjectExactFields(image, map[string]any{ "docker-manifest-digest": &digestString, }); err != nil { return err } - s.UntrustedDockerManifestDigest = digest.Digest(digestString) + s.untrustedDockerManifestDigest = digest.Digest(digestString) - return ParanoidUnmarshalJSONObjectExactFields(identity, map[string]interface{}{ - "docker-reference": &s.UntrustedDockerReference, + return ParanoidUnmarshalJSONObjectExactFields(identity, map[string]any{ + "docker-reference": &s.untrustedDockerReference, }) } @@ -190,10 +191,10 @@ func VerifySigstorePayload(publicKey crypto.PublicKey, unverifiedPayload []byte, if err := json.Unmarshal(unverifiedPayload, &unmatchedPayload); err != nil { return nil, NewInvalidSignatureError(err.Error()) } - if err := rules.ValidateSignedDockerManifestDigest(unmatchedPayload.UntrustedDockerManifestDigest); err != nil { + if err := rules.ValidateSignedDockerManifestDigest(unmatchedPayload.untrustedDockerManifestDigest); err != nil { return nil, err } - if err := rules.ValidateSignedDockerReference(unmatchedPayload.UntrustedDockerReference); err != nil { + if err := rules.ValidateSignedDockerReference(unmatchedPayload.untrustedDockerReference); err != nil { return nil, err } // SigstorePayloadAcceptanceRules have accepted this value. diff --git a/signature/internal/sigstore_payload_test.go b/signature/internal/sigstore_payload_test.go index 1b22c886c1..eaee202a34 100644 --- a/signature/internal/sigstore_payload_test.go +++ b/signature/internal/sigstore_payload_test.go @@ -16,16 +16,14 @@ import ( "github.com/stretchr/testify/require" ) -type mSI map[string]interface{} // To minimize typing the long name - // A short-hand way to get a JSON object field value or panic. No error handling done, we know // what we are working with, a panic in a test is good enough, and fitting test cases on a single line // is a priority. -func x(m mSI, fields ...string) mSI { +func x(m mSA, fields ...string) mSA { for _, field := range fields { - // Not .(mSI) because type assertion of an unnamed type to a named type always fails (the types + // Not .(mSA) because type assertion of an unnamed type to a named type always fails (the types // are not "identical"), but the assignment is fine because they are "assignable". - m = m[field].(map[string]interface{}) + m = m[field].(map[string]any) } return m } @@ -33,14 +31,14 @@ func x(m mSI, fields ...string) mSI { func TestNewUntrustedSigstorePayload(t *testing.T) { timeBefore := time.Now() sig := NewUntrustedSigstorePayload(TestImageManifestDigest, TestImageSignatureReference) - assert.Equal(t, TestImageManifestDigest, sig.UntrustedDockerManifestDigest) - assert.Equal(t, TestImageSignatureReference, sig.UntrustedDockerReference) - require.NotNil(t, sig.UntrustedCreatorID) - assert.Equal(t, "containers/image "+version.Version, *sig.UntrustedCreatorID) - require.NotNil(t, sig.UntrustedTimestamp) + assert.Equal(t, TestImageManifestDigest, sig.untrustedDockerManifestDigest) + assert.Equal(t, TestImageSignatureReference, sig.untrustedDockerReference) + require.NotNil(t, sig.untrustedCreatorID) + assert.Equal(t, "containers/image "+version.Version, *sig.untrustedCreatorID) + require.NotNil(t, sig.untrustedTimestamp) timeAfter := time.Now() - assert.True(t, timeBefore.Unix() <= *sig.UntrustedTimestamp) - assert.True(t, *sig.UntrustedTimestamp <= timeAfter.Unix()) + assert.True(t, timeBefore.Unix() <= *sig.untrustedTimestamp) + assert.True(t, *sig.untrustedTimestamp <= timeAfter.Unix()) } func TestUntrustedSigstorePayloadMarshalJSON(t *testing.T) { @@ -62,17 +60,17 @@ func TestUntrustedSigstorePayloadMarshalJSON(t *testing.T) { }{ { UntrustedSigstorePayload{ - UntrustedDockerManifestDigest: "digest!@#", - UntrustedDockerReference: "reference#@!", - UntrustedCreatorID: &creatorID, - UntrustedTimestamp: ×tamp, + untrustedDockerManifestDigest: "digest!@#", + untrustedDockerReference: "reference#@!", + untrustedCreatorID: &creatorID, + untrustedTimestamp: ×tamp, }, "{\"critical\":{\"identity\":{\"docker-reference\":\"reference#@!\"},\"image\":{\"docker-manifest-digest\":\"digest!@#\"},\"type\":\"cosign container image signature\"},\"optional\":{\"creator\":\"CREATOR\",\"timestamp\":1484683104}}", }, { UntrustedSigstorePayload{ - UntrustedDockerManifestDigest: "digest!@#", - UntrustedDockerReference: "reference#@!", + untrustedDockerManifestDigest: "digest!@#", + untrustedDockerReference: "reference#@!", }, "{\"critical\":{\"identity\":{\"docker-reference\":\"reference#@!\"},\"image\":{\"docker-manifest-digest\":\"digest!@#\"},\"type\":\"cosign container image signature\"},\"optional\":{}}", }, @@ -88,19 +86,6 @@ func TestUntrustedSigstorePayloadMarshalJSON(t *testing.T) { } } -// Return the result of modifying validJSON with fn -func modifiedUntrustedSigstorePayloadJSON(t *testing.T, validJSON []byte, modifyFn func(mSI)) []byte { - var tmp mSI - err := json.Unmarshal(validJSON, &tmp) - require.NoError(t, err) - - modifyFn(tmp) - - modifiedJSON, err := json.Marshal(tmp) - require.NoError(t, err) - return modifiedJSON -} - // Verify that input can be unmarshaled as an UntrustedSigstorePayload. func successfullyUnmarshalUntrustedSigstorePayload(t *testing.T, input []byte) UntrustedSigstorePayload { var s UntrustedSigstorePayload @@ -140,72 +125,72 @@ func TestUntrustedSigstorePayloadUnmarshalJSON(t *testing.T) { // A /usr/bin/cosign-generated payload is handled correctly s = successfullyUnmarshalUntrustedSigstorePayload(t, []byte(`{"critical":{"identity":{"docker-reference":"192.168.64.2:5000/cosign-signed-multi"},"image":{"docker-manifest-digest":"sha256:43955d6857268cc948ae9b370b221091057de83c4962da0826f9a2bdc9bd6b44"},"type":"cosign container image signature"},"optional":null}`)) assert.Equal(t, UntrustedSigstorePayload{ - UntrustedDockerManifestDigest: "sha256:43955d6857268cc948ae9b370b221091057de83c4962da0826f9a2bdc9bd6b44", - UntrustedDockerReference: "192.168.64.2:5000/cosign-signed-multi", - UntrustedCreatorID: nil, - UntrustedTimestamp: nil, + untrustedDockerManifestDigest: "sha256:43955d6857268cc948ae9b370b221091057de83c4962da0826f9a2bdc9bd6b44", + untrustedDockerReference: "192.168.64.2:5000/cosign-signed-multi", + untrustedCreatorID: nil, + untrustedTimestamp: nil, }, s) // Various ways to corrupt the JSON - breakFns := []func(mSI){ + breakFns := []func(mSA){ // A top-level field is missing - func(v mSI) { delete(v, "critical") }, - func(v mSI) { delete(v, "optional") }, + func(v mSA) { delete(v, "critical") }, + func(v mSA) { delete(v, "optional") }, // Extra top-level sub-object - func(v mSI) { v["unexpected"] = 1 }, + func(v mSA) { v["unexpected"] = 1 }, // "critical" not an object - func(v mSI) { v["critical"] = 1 }, + func(v mSA) { v["critical"] = 1 }, // "optional" not an object - func(v mSI) { v["optional"] = 1 }, + func(v mSA) { v["optional"] = 1 }, // A field of "critical" is missing - func(v mSI) { delete(x(v, "critical"), "type") }, - func(v mSI) { delete(x(v, "critical"), "image") }, - func(v mSI) { delete(x(v, "critical"), "identity") }, + func(v mSA) { delete(x(v, "critical"), "type") }, + func(v mSA) { delete(x(v, "critical"), "image") }, + func(v mSA) { delete(x(v, "critical"), "identity") }, // Extra field of "critical" - func(v mSI) { x(v, "critical")["unexpected"] = 1 }, + func(v mSA) { x(v, "critical")["unexpected"] = 1 }, // Invalid "type" - func(v mSI) { x(v, "critical")["type"] = 1 }, - func(v mSI) { x(v, "critical")["type"] = "unexpected" }, + func(v mSA) { x(v, "critical")["type"] = 1 }, + func(v mSA) { x(v, "critical")["type"] = "unexpected" }, // Invalid "image" object - func(v mSI) { x(v, "critical")["image"] = 1 }, - func(v mSI) { delete(x(v, "critical", "image"), "docker-manifest-digest") }, - func(v mSI) { x(v, "critical", "image")["unexpected"] = 1 }, + func(v mSA) { x(v, "critical")["image"] = 1 }, + func(v mSA) { delete(x(v, "critical", "image"), "docker-manifest-digest") }, + func(v mSA) { x(v, "critical", "image")["unexpected"] = 1 }, // Invalid "docker-manifest-digest" - func(v mSI) { x(v, "critical", "image")["docker-manifest-digest"] = 1 }, + func(v mSA) { x(v, "critical", "image")["docker-manifest-digest"] = 1 }, // Invalid "identity" object - func(v mSI) { x(v, "critical")["identity"] = 1 }, - func(v mSI) { delete(x(v, "critical", "identity"), "docker-reference") }, - func(v mSI) { x(v, "critical", "identity")["unexpected"] = 1 }, + func(v mSA) { x(v, "critical")["identity"] = 1 }, + func(v mSA) { delete(x(v, "critical", "identity"), "docker-reference") }, + func(v mSA) { x(v, "critical", "identity")["unexpected"] = 1 }, // Invalid "docker-reference" - func(v mSI) { x(v, "critical", "identity")["docker-reference"] = 1 }, + func(v mSA) { x(v, "critical", "identity")["docker-reference"] = 1 }, // Invalid "creator" - func(v mSI) { x(v, "optional")["creator"] = 1 }, + func(v mSA) { x(v, "optional")["creator"] = 1 }, // Invalid "timestamp" - func(v mSI) { x(v, "optional")["timestamp"] = "unexpected" }, - func(v mSI) { x(v, "optional")["timestamp"] = 0.5 }, // Fractional input + func(v mSA) { x(v, "optional")["timestamp"] = "unexpected" }, + func(v mSA) { x(v, "optional")["timestamp"] = 0.5 }, // Fractional input } for _, fn := range breakFns { - testJSON := modifiedUntrustedSigstorePayloadJSON(t, validJSON, fn) + testJSON := modifiedJSON(t, validJSON, fn) assertUnmarshalUntrustedSigstorePayloadFails(t, testJSON) } // Modifications to unrecognized fields in "optional" are allowed and ignored - allowedModificationFns := []func(mSI){ + allowedModificationFns := []func(mSA){ // Add an optional field - func(v mSI) { x(v, "optional")["unexpected"] = 1 }, + func(v mSA) { x(v, "optional")["unexpected"] = 1 }, } for _, fn := range allowedModificationFns { - testJSON := modifiedUntrustedSigstorePayloadJSON(t, validJSON, fn) + testJSON := modifiedJSON(t, validJSON, fn) s := successfullyUnmarshalUntrustedSigstorePayload(t, testJSON) assert.Equal(t, validSig, s) } // Optional fields can be missing validSig = UntrustedSigstorePayload{ - UntrustedDockerManifestDigest: "digest!@#", - UntrustedDockerReference: "reference#@!", - UntrustedCreatorID: nil, - UntrustedTimestamp: nil, + untrustedDockerManifestDigest: "digest!@#", + untrustedDockerReference: "reference#@!", + untrustedCreatorID: nil, + untrustedTimestamp: nil, } validJSON, err = validSig.MarshalJSON() require.NoError(t, err) @@ -262,10 +247,10 @@ func TestVerifySigstorePayload(t *testing.T) { res, err := VerifySigstorePayload(publicKey, sigstoreSig.UntrustedPayload(), cryptoBase64Sig, recordingRules) require.NoError(t, err) assert.Equal(t, res, &UntrustedSigstorePayload{ - UntrustedDockerManifestDigest: TestSigstoreManifestDigest, - UntrustedDockerReference: TestSigstoreSignatureReference, - UntrustedCreatorID: nil, - UntrustedTimestamp: nil, + untrustedDockerManifestDigest: TestSigstoreManifestDigest, + untrustedDockerReference: TestSigstoreSignatureReference, + untrustedCreatorID: nil, + untrustedTimestamp: nil, }) assert.Equal(t, signatureData, recorded) @@ -273,7 +258,7 @@ func TestVerifySigstorePayload(t *testing.T) { // Invalid verifier recorded = acceptanceData{} - invalidPublicKey := struct{}{} // crypto.PublicKey is, for some reason, just an interface{}, so this is acceptable. + invalidPublicKey := struct{}{} // crypto.PublicKey is, for some reason, just an any, so this is acceptable. res, err = VerifySigstorePayload(invalidPublicKey, sigstoreSig.UntrustedPayload(), cryptoBase64Sig, recordingRules) assert.Error(t, err) assert.Nil(t, res) diff --git a/signature/internal/testdata/rekor-cert b/signature/internal/testdata/rekor-cert new file mode 100644 index 0000000000..734b8bb200 --- /dev/null +++ b/signature/internal/testdata/rekor-cert @@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE----- +MIICnTCCAiOgAwIBAgIUG45uaC2z8VvuOwzzm79RPfckoxwwCgYIKoZIzj0EAwMw +NzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRl +cm1lZGlhdGUwHhcNMjIxMjEyMTg0ODE4WhcNMjIxMjEyMTg1ODE4WjAAMFkwEwYH +KoZIzj0CAQYIKoZIzj0DAQcDQgAEJBurkhSRmHukHVh3VA8nYuSWqZ4ltafDwyWl +c3c9NaLovmu+NC4NiUtMLifL/P3nqedbnctKBuYmfISGiZlVyKOCAUIwggE+MA4G +A1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUAmvG +4K6lNk/sk/f4aiahjYRqR+cwHwYDVR0jBBgwFoAU39Ppz1YkEZb5qNjpKFWixi4Y +ZD8wHQYDVR0RAQH/BBMwEYEPbWl0ckByZWRoYXQuY29tMCwGCisGAQQBg78wAQEE +Hmh0dHBzOi8vZ2l0aHViLmNvbS9sb2dpbi9vYXV0aDCBiQYKKwYBBAHWeQIEAgR7 +BHkAdwB1AN09MGrGxxEyYxkeHJlnNwKiSl643jyt/4eKcoAvKe6OAAABhQeqlwUA +AAQDAEYwRAIgVi2SvN5ReGYiOShY8LFSRG5D6oATKqHb6kN/DJh2rAUCIANS6Aqp +xaXiHwTHUr3xPSGa5i6hywmbbRC6N0kIeDM6MAoGCCqGSM49BAMDA2gAMGUCMCLu +cZtESIN0lm64Co/bZ68CCRkWlktX4mmJiRhKi9c9QD62C5SuZhc0vvo6MOKmTQIx +AIo/MozZeoU7rdNj0pZKuBeCmMqmJaDSsw19tKi/b1pEmuw+Sf2BI25GgIbKztG1 +9w== +-----END CERTIFICATE----- diff --git a/signature/internal/testdata/rekor-payload b/signature/internal/testdata/rekor-payload new file mode 100644 index 0000000000..b3cf6c40d6 --- /dev/null +++ b/signature/internal/testdata/rekor-payload @@ -0,0 +1 @@ +{"critical":{"identity":{"docker-reference":"192.168.64.2:5000/test-repo-2100826901021538654/alpine"},"image":{"docker-manifest-digest":"sha256:fa93b01658e3a5a1686dc3ae55f170d8de487006fb53a28efcd12ab0710a2e5f"},"type":"cosign container image signature"},"optional":null} \ No newline at end of file diff --git a/signature/internal/testdata/rekor-set b/signature/internal/testdata/rekor-set new file mode 100644 index 0000000000..04ca1e5487 --- /dev/null +++ b/signature/internal/testdata/rekor-set @@ -0,0 +1 @@ +{"SignedEntryTimestamp":"MEYCIQDdeujdGLpMTgFdew9wsSJ3WF7olX9PawgzGeX2RmJd8QIhAPxGJf+HjUFVpQc0hgPaUSK8LsONJ08fZFEBVKDeLj4S","Payload":{"body":"eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiIwZmQxZTk4MzJjYzVhNWY1MDJlODAwZmU5Y2RlZWZiZDMxMzYyZGYxNmZlOGMyMjUwZDMwOGFlYTNmYjFmYzY5In19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FUUNJRUpjOTZlMDQxVkFoS0EwM1N2ZkNZYldvZElNSVFQeUF0V3lEUDRGblBxcEFpQWFJUzYwRWpoUkRoU2Fub0Zzb0l5OGZLcXFLZVc1cHMvdExYU0dwYXlpMmc9PSIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVTnVWRU5EUVdsUFowRjNTVUpCWjBsVlJ6UTFkV0ZETW5vNFZuWjFUM2Q2ZW0wM09WSlFabU5yYjNoM2QwTm5XVWxMYjFwSmVtb3dSVUYzVFhjS1RucEZWazFDVFVkQk1WVkZRMmhOVFdNeWJHNWpNMUoyWTIxVmRWcEhWakpOVWpSM1NFRlpSRlpSVVVSRmVGWjZZVmRrZW1SSE9YbGFVekZ3WW01U2JBcGpiVEZzV2tkc2FHUkhWWGRJYUdOT1RXcEplRTFxUlhsTlZHY3dUMFJGTkZkb1kwNU5ha2w0VFdwRmVVMVVaekZQUkVVMFYycEJRVTFHYTNkRmQxbElDa3R2V2tsNmFqQkRRVkZaU1V0dldrbDZhakJFUVZGalJGRm5RVVZLUW5WeWEyaFRVbTFJZFd0SVZtZ3pWa0U0YmxsMVUxZHhXalJzZEdGbVJIZDVWMndLWXpOak9VNWhURzkyYlhVclRrTTBUbWxWZEUxTWFXWk1MMUF6Ym5GbFpHSnVZM1JMUW5WWmJXWkpVMGRwV214V2VVdFBRMEZWU1hkblowVXJUVUUwUndwQk1WVmtSSGRGUWk5M1VVVkJkMGxJWjBSQlZFSm5UbFpJVTFWRlJFUkJTMEpuWjNKQ1owVkdRbEZqUkVGNlFXUkNaMDVXU0ZFMFJVWm5VVlZCYlhaSENqUkxObXhPYXk5emF5OW1OR0ZwWVdocVdWSnhVaXRqZDBoM1dVUldVakJxUWtKbmQwWnZRVlV6T1ZCd2VqRlphMFZhWWpWeFRtcHdTMFpYYVhocE5Ga0tXa1E0ZDBoUldVUldVakJTUVZGSUwwSkNUWGRGV1VWUVlsZHNNR05yUW5sYVYxSnZXVmhSZFZreU9YUk5RM2RIUTJselIwRlJVVUpuTnpoM1FWRkZSUXBJYldnd1pFaENlazlwT0haYU1td3dZVWhXYVV4dFRuWmlVemx6WWpKa2NHSnBPWFpaV0ZZd1lVUkRRbWxSV1V0TGQxbENRa0ZJVjJWUlNVVkJaMUkzQ2tKSWEwRmtkMEl4UVU0d09VMUhja2Q0ZUVWNVdYaHJaVWhLYkc1T2QwdHBVMncyTkROcWVYUXZOR1ZMWTI5QmRrdGxOazlCUVVGQ2FGRmxjV3gzVlVFS1FVRlJSRUZGV1hkU1FVbG5WbWt5VTNaT05WSmxSMWxwVDFOb1dUaE1SbE5TUnpWRU5tOUJWRXR4U0dJMmEwNHZSRXBvTW5KQlZVTkpRVTVUTmtGeGNBcDRZVmhwU0hkVVNGVnlNM2hRVTBkaE5XazJhSGwzYldKaVVrTTJUakJyU1dWRVRUWk5RVzlIUTBOeFIxTk5ORGxDUVUxRVFUSm5RVTFIVlVOTlEweDFDbU5hZEVWVFNVNHdiRzAyTkVOdkwySmFOamhEUTFKclYyeHJkRmcwYlcxS2FWSm9TMms1WXpsUlJEWXlRelZUZFZwb1l6QjJkbTgyVFU5TGJWUlJTWGdLUVVsdkwwMXZlbHBsYjFVM2NtUk9hakJ3V2t0MVFtVkRiVTF4YlVwaFJGTnpkekU1ZEV0cEwySXhjRVZ0ZFhjclUyWXlRa2t5TlVkblNXSkxlblJITVFvNWR6MDlDaTB0TFMwdFJVNUVJRU5GVWxSSlJrbERRVlJGTFMwdExTMEsifX19fQ==","integratedTime":1670870899,"logIndex":8949589,"logID":"c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d"}} \ No newline at end of file diff --git a/signature/internal/testdata/rekor-sig b/signature/internal/testdata/rekor-sig new file mode 100644 index 0000000000..5735a2341a --- /dev/null +++ b/signature/internal/testdata/rekor-sig @@ -0,0 +1 @@ +MEQCIEJc96e041VAhKA03SvfCYbWodIMIQPyAtWyDP4FnPqpAiAaIS60EjhRDhSanoFsoIy8fKqqKeW5ps/tLXSGpayi2g== \ No newline at end of file diff --git a/signature/internal/testdata/rekor.pub b/signature/internal/testdata/rekor.pub new file mode 100644 index 0000000000..050ef60149 --- /dev/null +++ b/signature/internal/testdata/rekor.pub @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE2G2Y+2tabdTV5BcGiBIx0a9fAFwr +kBbmLSGtks4L3qX6yYY0zufBnhC8Ur/iy55GhWP/9A/bY2LhC30M9+RYtw== +-----END PUBLIC KEY----- diff --git a/signature/mechanism_gpgme_test.go b/signature/mechanism_gpgme_test.go index b05c28c9f8..82ca998937 100644 --- a/signature/mechanism_gpgme_test.go +++ b/signature/mechanism_gpgme_test.go @@ -7,10 +7,21 @@ import ( "os" "testing" + "github.com/containers/image/v5/internal/testing/gpgagent" + "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) +// Ensure we don’t leave around GPG agent processes. +func TestMain(m *testing.M) { + code := m.Run() + if err := gpgagent.KillGPGAgent(testGPGHomeDirectory); err != nil { + logrus.Warnf("Error killing GPG agent: %v", err) + } + os.Exit(code) +} + func TestGPGMESigningMechanismClose(t *testing.T) { // Closing an ephemeral mechanism removes the directory. // (The non-ephemeral case is tested in the common TestGPGSigningMechanismClose) diff --git a/signature/mechanism_test.go b/signature/mechanism_test.go index 340d8c7e54..ef67db6b99 100644 --- a/signature/mechanism_test.go +++ b/signature/mechanism_test.go @@ -203,7 +203,7 @@ func TestGPGSigningMechanismSign(t *testing.T) { // The various GPG/GPGME failures cases are not obviously easy to reach. } -func assertSigningError(t *testing.T, content []byte, fingerprint string, err error, msgAndArgs ...interface{}) { +func assertSigningError(t *testing.T, content []byte, fingerprint string, err error, msgAndArgs ...any) { assert.Error(t, err, msgAndArgs...) assert.Nil(t, content, msgAndArgs...) assert.Empty(t, fingerprint, msgAndArgs...) diff --git a/signature/policy_config.go b/signature/policy_config.go index f8fdce2da5..7eb5cab7d8 100644 --- a/signature/policy_config.go +++ b/signature/policy_config.go @@ -19,13 +19,13 @@ import ( "fmt" "os" "path/filepath" - "regexp" "github.com/containers/image/v5/docker/reference" "github.com/containers/image/v5/signature/internal" "github.com/containers/image/v5/transports" "github.com/containers/image/v5/types" "github.com/containers/storage/pkg/homedir" + "github.com/containers/storage/pkg/regexp" ) // systemDefaultPolicyPath is the policy path used for DefaultPolicy(). @@ -104,7 +104,7 @@ var _ json.Unmarshaler = (*Policy)(nil) func (p *Policy) UnmarshalJSON(data []byte) error { *p = Policy{} transports := policyTransportsMap{} - if err := internal.ParanoidUnmarshalJSONObject(data, func(key string) interface{} { + if err := internal.ParanoidUnmarshalJSONObject(data, func(key string) any { switch key { case "default": return &p.Default @@ -135,7 +135,7 @@ func (m *policyTransportsMap) UnmarshalJSON(data []byte) error { // We can't unmarshal directly into map values because it is not possible to take an address of a map value. // So, use a temporary map of pointers-to-slices and convert. tmpMap := map[string]*PolicyTransportScopes{} - if err := internal.ParanoidUnmarshalJSONObject(data, func(key string) interface{} { + if err := internal.ParanoidUnmarshalJSONObject(data, func(key string) any { // transport can be nil transport := transports.Get(key) // internal.ParanoidUnmarshalJSONObject detects key duplication for us, check just to be safe. @@ -181,7 +181,7 @@ func (m *policyTransportScopesWithTransport) UnmarshalJSON(data []byte) error { // We can't unmarshal directly into map values because it is not possible to take an address of a map value. // So, use a temporary map of pointers-to-slices and convert. tmpMap := map[string]*PolicyRequirements{} - if err := internal.ParanoidUnmarshalJSONObject(data, func(key string) interface{} { + if err := internal.ParanoidUnmarshalJSONObject(data, func(key string) any { // internal.ParanoidUnmarshalJSONObject detects key duplication for us, check just to be safe. if _, ok := tmpMap[key]; ok { return nil @@ -271,7 +271,7 @@ var _ json.Unmarshaler = (*prInsecureAcceptAnything)(nil) func (pr *prInsecureAcceptAnything) UnmarshalJSON(data []byte) error { *pr = prInsecureAcceptAnything{} var tmp prInsecureAcceptAnything - if err := internal.ParanoidUnmarshalJSONObjectExactFields(data, map[string]interface{}{ + if err := internal.ParanoidUnmarshalJSONObjectExactFields(data, map[string]any{ "type": &tmp.Type, }); err != nil { return err @@ -301,7 +301,7 @@ var _ json.Unmarshaler = (*prReject)(nil) func (pr *prReject) UnmarshalJSON(data []byte) error { *pr = prReject{} var tmp prReject - if err := internal.ParanoidUnmarshalJSONObjectExactFields(data, map[string]interface{}{ + if err := internal.ParanoidUnmarshalJSONObjectExactFields(data, map[string]any{ "type": &tmp.Type, }); err != nil { return err @@ -384,7 +384,7 @@ func (pr *prSignedBy) UnmarshalJSON(data []byte) error { var tmp prSignedBy var gotKeyPath, gotKeyPaths, gotKeyData = false, false, false var signedIdentity json.RawMessage - if err := internal.ParanoidUnmarshalJSONObject(data, func(key string) interface{} { + if err := internal.ParanoidUnmarshalJSONObject(data, func(key string) any { switch key { case "type": return &tmp.Type @@ -495,7 +495,7 @@ func (pr *prSignedBaseLayer) UnmarshalJSON(data []byte) error { *pr = prSignedBaseLayer{} var tmp prSignedBaseLayer var baseLayerIdentity json.RawMessage - if err := internal.ParanoidUnmarshalJSONObjectExactFields(data, map[string]interface{}{ + if err := internal.ParanoidUnmarshalJSONObjectExactFields(data, map[string]any{ "type": &tmp.Type, "baseLayerIdentity": &baseLayerIdentity, }); err != nil { @@ -518,107 +518,6 @@ func (pr *prSignedBaseLayer) UnmarshalJSON(data []byte) error { return nil } -// newPRSigstoreSigned returns a new prSigstoreSigned if parameters are valid. -func newPRSigstoreSigned(keyPath string, keyData []byte, signedIdentity PolicyReferenceMatch) (*prSigstoreSigned, error) { - if len(keyPath) > 0 && len(keyData) > 0 { - return nil, InvalidPolicyFormatError("keyType and keyData cannot be used simultaneously") - } - if signedIdentity == nil { - return nil, InvalidPolicyFormatError("signedIdentity not specified") - } - return &prSigstoreSigned{ - prCommon: prCommon{Type: prTypeSigstoreSigned}, - KeyPath: keyPath, - KeyData: keyData, - SignedIdentity: signedIdentity, - }, nil -} - -// newPRSigstoreSignedKeyPath is NewPRSigstoreSignedKeyPath, except it returns the private type. -func newPRSigstoreSignedKeyPath(keyPath string, signedIdentity PolicyReferenceMatch) (*prSigstoreSigned, error) { - return newPRSigstoreSigned(keyPath, nil, signedIdentity) -} - -// NewPRSigstoreSignedKeyPath returns a new "sigstoreSigned" PolicyRequirement using a KeyPath -func NewPRSigstoreSignedKeyPath(keyPath string, signedIdentity PolicyReferenceMatch) (PolicyRequirement, error) { - return newPRSigstoreSignedKeyPath(keyPath, signedIdentity) -} - -// newPRSigstoreSignedKeyData is NewPRSigstoreSignedKeyData, except it returns the private type. -func newPRSigstoreSignedKeyData(keyData []byte, signedIdentity PolicyReferenceMatch) (*prSigstoreSigned, error) { - return newPRSigstoreSigned("", keyData, signedIdentity) -} - -// NewPRSigstoreSignedKeyData returns a new "sigstoreSigned" PolicyRequirement using a KeyData -func NewPRSigstoreSignedKeyData(keyData []byte, signedIdentity PolicyReferenceMatch) (PolicyRequirement, error) { - return newPRSigstoreSignedKeyData(keyData, signedIdentity) -} - -// Compile-time check that prSigstoreSigned implements json.Unmarshaler. -var _ json.Unmarshaler = (*prSigstoreSigned)(nil) - -// UnmarshalJSON implements the json.Unmarshaler interface. -func (pr *prSigstoreSigned) UnmarshalJSON(data []byte) error { - *pr = prSigstoreSigned{} - var tmp prSigstoreSigned - var gotKeyPath, gotKeyData = false, false - var signedIdentity json.RawMessage - if err := internal.ParanoidUnmarshalJSONObject(data, func(key string) interface{} { - switch key { - case "type": - return &tmp.Type - case "keyPath": - gotKeyPath = true - return &tmp.KeyPath - case "keyData": - gotKeyData = true - return &tmp.KeyData - case "signedIdentity": - return &signedIdentity - default: - return nil - } - }); err != nil { - return err - } - - if tmp.Type != prTypeSigstoreSigned { - return InvalidPolicyFormatError(fmt.Sprintf("Unexpected policy requirement type \"%s\"", tmp.Type)) - } - if signedIdentity == nil { - tmp.SignedIdentity = NewPRMMatchRepoDigestOrExact() - } else { - si, err := newPolicyReferenceMatchFromJSON(signedIdentity) - if err != nil { - return err - } - tmp.SignedIdentity = si - } - - var res *prSigstoreSigned - var err error - switch { - case gotKeyPath && gotKeyData: - return InvalidPolicyFormatError("keyPath and keyData cannot be used simultaneously") - case gotKeyPath && !gotKeyData: - res, err = newPRSigstoreSignedKeyPath(tmp.KeyPath, tmp.SignedIdentity) - case !gotKeyPath && gotKeyData: - res, err = newPRSigstoreSignedKeyData(tmp.KeyData, tmp.SignedIdentity) - case !gotKeyPath && !gotKeyData: - return InvalidPolicyFormatError("At least one of keyPath and keyData must be specified") - default: // Coverage: This should never happen - return fmt.Errorf("Impossible keyPath/keyData presence combination!?") - } - if err != nil { - // Coverage: This cannot currently happen, creating a prSigstoreSigned only fails - // if signedIdentity is nil, which we replace with a default above. - return err - } - *pr = *res - - return nil -} - // newPolicyReferenceMatchFromJSON parses JSON data into a PolicyReferenceMatch implementation. func newPolicyReferenceMatchFromJSON(data []byte) (PolicyReferenceMatch, error) { var typeField prmCommon @@ -665,7 +564,7 @@ var _ json.Unmarshaler = (*prmMatchExact)(nil) func (prm *prmMatchExact) UnmarshalJSON(data []byte) error { *prm = prmMatchExact{} var tmp prmMatchExact - if err := internal.ParanoidUnmarshalJSONObjectExactFields(data, map[string]interface{}{ + if err := internal.ParanoidUnmarshalJSONObjectExactFields(data, map[string]any{ "type": &tmp.Type, }); err != nil { return err @@ -695,7 +594,7 @@ var _ json.Unmarshaler = (*prmMatchRepoDigestOrExact)(nil) func (prm *prmMatchRepoDigestOrExact) UnmarshalJSON(data []byte) error { *prm = prmMatchRepoDigestOrExact{} var tmp prmMatchRepoDigestOrExact - if err := internal.ParanoidUnmarshalJSONObjectExactFields(data, map[string]interface{}{ + if err := internal.ParanoidUnmarshalJSONObjectExactFields(data, map[string]any{ "type": &tmp.Type, }); err != nil { return err @@ -725,7 +624,7 @@ var _ json.Unmarshaler = (*prmMatchRepository)(nil) func (prm *prmMatchRepository) UnmarshalJSON(data []byte) error { *prm = prmMatchRepository{} var tmp prmMatchRepository - if err := internal.ParanoidUnmarshalJSONObjectExactFields(data, map[string]interface{}{ + if err := internal.ParanoidUnmarshalJSONObjectExactFields(data, map[string]any{ "type": &tmp.Type, }); err != nil { return err @@ -765,7 +664,7 @@ var _ json.Unmarshaler = (*prmExactReference)(nil) func (prm *prmExactReference) UnmarshalJSON(data []byte) error { *prm = prmExactReference{} var tmp prmExactReference - if err := internal.ParanoidUnmarshalJSONObjectExactFields(data, map[string]interface{}{ + if err := internal.ParanoidUnmarshalJSONObjectExactFields(data, map[string]any{ "type": &tmp.Type, "dockerReference": &tmp.DockerReference, }); err != nil { @@ -807,7 +706,7 @@ var _ json.Unmarshaler = (*prmExactRepository)(nil) func (prm *prmExactRepository) UnmarshalJSON(data []byte) error { *prm = prmExactRepository{} var tmp prmExactRepository - if err := internal.ParanoidUnmarshalJSONObjectExactFields(data, map[string]interface{}{ + if err := internal.ParanoidUnmarshalJSONObjectExactFields(data, map[string]any{ "type": &tmp.Type, "dockerRepository": &tmp.DockerRepository, }); err != nil { @@ -829,12 +728,12 @@ func (prm *prmExactRepository) UnmarshalJSON(data []byte) error { // Private objects for validateIdentityRemappingPrefix var ( // remapIdentityDomainRegexp matches exactly a reference domain (name[:port]) - remapIdentityDomainRegexp = regexp.MustCompile("^" + reference.DomainRegexp.String() + "$") + remapIdentityDomainRegexp = regexp.Delayed("^" + reference.DomainRegexp.String() + "$") // remapIdentityDomainPrefixRegexp matches a reference that starts with a domain; // we need this because reference.NameRegexp accepts short names with docker.io implied. - remapIdentityDomainPrefixRegexp = regexp.MustCompile("^" + reference.DomainRegexp.String() + "/") + remapIdentityDomainPrefixRegexp = regexp.Delayed("^" + reference.DomainRegexp.String() + "/") // remapIdentityNameRegexp matches exactly a reference.Named name (possibly unnormalized) - remapIdentityNameRegexp = regexp.MustCompile("^" + reference.NameRegexp.String() + "$") + remapIdentityNameRegexp = regexp.Delayed("^" + reference.NameRegexp.String() + "$") ) // validateIdentityRemappingPrefix returns an InvalidPolicyFormatError if s is detected to be invalid @@ -879,7 +778,7 @@ var _ json.Unmarshaler = (*prmRemapIdentity)(nil) func (prm *prmRemapIdentity) UnmarshalJSON(data []byte) error { *prm = prmRemapIdentity{} var tmp prmRemapIdentity - if err := internal.ParanoidUnmarshalJSONObjectExactFields(data, map[string]interface{}{ + if err := internal.ParanoidUnmarshalJSONObjectExactFields(data, map[string]any{ "type": &tmp.Type, "prefix": &tmp.Prefix, "signedPrefix": &tmp.SignedPrefix, diff --git a/signature/policy_config_sigstore.go b/signature/policy_config_sigstore.go new file mode 100644 index 0000000000..d8c6a97f1b --- /dev/null +++ b/signature/policy_config_sigstore.go @@ -0,0 +1,343 @@ +package signature + +import ( + "encoding/json" + "errors" + "fmt" + + "github.com/containers/image/v5/signature/internal" +) + +// PRSigstoreSignedOption is way to pass values to NewPRSigstoreSigned +type PRSigstoreSignedOption func(*prSigstoreSigned) error + +// PRSigstoreSignedWithKeyPath specifies a value for the "keyPath" field when calling NewPRSigstoreSigned. +func PRSigstoreSignedWithKeyPath(keyPath string) PRSigstoreSignedOption { + return func(pr *prSigstoreSigned) error { + if pr.KeyPath != "" { + return errors.New(`"keyPath" already specified`) + } + pr.KeyPath = keyPath + return nil + } +} + +// PRSigstoreSignedWithKeyData specifies a value for the "keyData" field when calling NewPRSigstoreSigned. +func PRSigstoreSignedWithKeyData(keyData []byte) PRSigstoreSignedOption { + return func(pr *prSigstoreSigned) error { + if pr.KeyData != nil { + return errors.New(`"keyData" already specified`) + } + pr.KeyData = keyData + return nil + } +} + +// PRSigstoreSignedWithFulcio specifies a value for the "fulcio" field when calling NewPRSigstoreSigned. +func PRSigstoreSignedWithFulcio(fulcio PRSigstoreSignedFulcio) PRSigstoreSignedOption { + return func(pr *prSigstoreSigned) error { + if pr.Fulcio != nil { + return errors.New(`"fulcio" already specified`) + } + pr.Fulcio = fulcio + return nil + } +} + +// PRSigstoreSignedWithRekorPublicKeyPath specifies a value for the "rekorPublicKeyPath" field when calling NewPRSigstoreSigned. +func PRSigstoreSignedWithRekorPublicKeyPath(rekorPublicKeyPath string) PRSigstoreSignedOption { + return func(pr *prSigstoreSigned) error { + if pr.RekorPublicKeyPath != "" { + return errors.New(`"rekorPublicKeyPath" already specified`) + } + pr.RekorPublicKeyPath = rekorPublicKeyPath + return nil + } +} + +// PRSigstoreSignedWithRekorPublicKeyData specifies a value for the "rekorPublicKeyData" field when calling NewPRSigstoreSigned. +func PRSigstoreSignedWithRekorPublicKeyData(rekorPublicKeyData []byte) PRSigstoreSignedOption { + return func(pr *prSigstoreSigned) error { + if pr.RekorPublicKeyData != nil { + return errors.New(`"rekorPublicKeyData" already specified`) + } + pr.RekorPublicKeyData = rekorPublicKeyData + return nil + } +} + +// PRSigstoreSignedWithSignedIdentity specifies a value for the "signedIdentity" field when calling NewPRSigstoreSigned. +func PRSigstoreSignedWithSignedIdentity(signedIdentity PolicyReferenceMatch) PRSigstoreSignedOption { + return func(pr *prSigstoreSigned) error { + if pr.SignedIdentity != nil { + return errors.New(`"signedIdentity" already specified`) + } + pr.SignedIdentity = signedIdentity + return nil + } +} + +// newPRSigstoreSigned is NewPRSigstoreSigned, except it returns the private type. +func newPRSigstoreSigned(options ...PRSigstoreSignedOption) (*prSigstoreSigned, error) { + res := prSigstoreSigned{ + prCommon: prCommon{Type: prTypeSigstoreSigned}, + } + for _, o := range options { + if err := o(&res); err != nil { + return nil, err + } + } + + keySources := 0 + if res.KeyPath != "" { + keySources++ + } + if res.KeyData != nil { + keySources++ + } + if res.Fulcio != nil { + keySources++ + } + if keySources != 1 { + return nil, InvalidPolicyFormatError("exactly one of keyPath, keyData and fulcio must be specified") + } + + if res.RekorPublicKeyPath != "" && res.RekorPublicKeyData != nil { + return nil, InvalidPolicyFormatError("rekorPublickeyType and rekorPublickeyData cannot be used simultaneously") + } + if res.Fulcio != nil && res.RekorPublicKeyPath == "" && res.RekorPublicKeyData == nil { + return nil, InvalidPolicyFormatError("At least one of RekorPublickeyPath and RekorPublickeyData must be specified if fulcio is used") + } + + if res.SignedIdentity == nil { + return nil, InvalidPolicyFormatError("signedIdentity not specified") + } + + return &res, nil +} + +// NewPRSigstoreSigned returns a new "sigstoreSigned" PolicyRequirement based on options. +func NewPRSigstoreSigned(options ...PRSigstoreSignedOption) (PolicyRequirement, error) { + return newPRSigstoreSigned(options...) +} + +// NewPRSigstoreSignedKeyPath returns a new "sigstoreSigned" PolicyRequirement using a KeyPath +func NewPRSigstoreSignedKeyPath(keyPath string, signedIdentity PolicyReferenceMatch) (PolicyRequirement, error) { + return NewPRSigstoreSigned( + PRSigstoreSignedWithKeyPath(keyPath), + PRSigstoreSignedWithSignedIdentity(signedIdentity), + ) +} + +// NewPRSigstoreSignedKeyData returns a new "sigstoreSigned" PolicyRequirement using a KeyData +func NewPRSigstoreSignedKeyData(keyData []byte, signedIdentity PolicyReferenceMatch) (PolicyRequirement, error) { + return NewPRSigstoreSigned( + PRSigstoreSignedWithKeyData(keyData), + PRSigstoreSignedWithSignedIdentity(signedIdentity), + ) +} + +// Compile-time check that prSigstoreSigned implements json.Unmarshaler. +var _ json.Unmarshaler = (*prSigstoreSigned)(nil) + +// UnmarshalJSON implements the json.Unmarshaler interface. +func (pr *prSigstoreSigned) UnmarshalJSON(data []byte) error { + *pr = prSigstoreSigned{} + var tmp prSigstoreSigned + var gotKeyPath, gotKeyData, gotFulcio, gotRekorPublicKeyPath, gotRekorPublicKeyData bool + var fulcio prSigstoreSignedFulcio + var signedIdentity json.RawMessage + if err := internal.ParanoidUnmarshalJSONObject(data, func(key string) any { + switch key { + case "type": + return &tmp.Type + case "keyPath": + gotKeyPath = true + return &tmp.KeyPath + case "keyData": + gotKeyData = true + return &tmp.KeyData + case "fulcio": + gotFulcio = true + return &fulcio + case "rekorPublicKeyPath": + gotRekorPublicKeyPath = true + return &tmp.RekorPublicKeyPath + case "rekorPublicKeyData": + gotRekorPublicKeyData = true + return &tmp.RekorPublicKeyData + case "signedIdentity": + return &signedIdentity + default: + return nil + } + }); err != nil { + return err + } + + if tmp.Type != prTypeSigstoreSigned { + return InvalidPolicyFormatError(fmt.Sprintf("Unexpected policy requirement type \"%s\"", tmp.Type)) + } + if signedIdentity == nil { + tmp.SignedIdentity = NewPRMMatchRepoDigestOrExact() + } else { + si, err := newPolicyReferenceMatchFromJSON(signedIdentity) + if err != nil { + return err + } + tmp.SignedIdentity = si + } + + var opts []PRSigstoreSignedOption + if gotKeyPath { + opts = append(opts, PRSigstoreSignedWithKeyPath(tmp.KeyPath)) + } + if gotKeyData { + opts = append(opts, PRSigstoreSignedWithKeyData(tmp.KeyData)) + } + if gotFulcio { + opts = append(opts, PRSigstoreSignedWithFulcio(&fulcio)) + } + if gotRekorPublicKeyPath { + opts = append(opts, PRSigstoreSignedWithRekorPublicKeyPath(tmp.RekorPublicKeyPath)) + } + if gotRekorPublicKeyData { + opts = append(opts, PRSigstoreSignedWithRekorPublicKeyData(tmp.RekorPublicKeyData)) + } + opts = append(opts, PRSigstoreSignedWithSignedIdentity(tmp.SignedIdentity)) + + res, err := newPRSigstoreSigned(opts...) + if err != nil { + return err + } + *pr = *res + return nil +} + +// PRSigstoreSignedFulcioOption is a way to pass values to NewPRSigstoreSignedFulcio +type PRSigstoreSignedFulcioOption func(*prSigstoreSignedFulcio) error + +// PRSigstoreSignedFulcioWithCAPath specifies a value for the "caPath" field when calling NewPRSigstoreSignedFulcio +func PRSigstoreSignedFulcioWithCAPath(caPath string) PRSigstoreSignedFulcioOption { + return func(f *prSigstoreSignedFulcio) error { + if f.CAPath != "" { + return errors.New(`"caPath" already specified`) + } + f.CAPath = caPath + return nil + } +} + +// PRSigstoreSignedFulcioWithCAData specifies a value for the "caData" field when calling NewPRSigstoreSignedFulcio +func PRSigstoreSignedFulcioWithCAData(caData []byte) PRSigstoreSignedFulcioOption { + return func(f *prSigstoreSignedFulcio) error { + if f.CAData != nil { + return errors.New(`"caData" already specified`) + } + f.CAData = caData + return nil + } +} + +// PRSigstoreSignedFulcioWithOIDCIssuer specifies a value for the "oidcIssuer" field when calling NewPRSigstoreSignedFulcio +func PRSigstoreSignedFulcioWithOIDCIssuer(oidcIssuer string) PRSigstoreSignedFulcioOption { + return func(f *prSigstoreSignedFulcio) error { + if f.OIDCIssuer != "" { + return errors.New(`"oidcIssuer" already specified`) + } + f.OIDCIssuer = oidcIssuer + return nil + } +} + +// PRSigstoreSignedFulcioWithSubjectEmail specifies a value for the "subjectEmail" field when calling NewPRSigstoreSignedFulcio +func PRSigstoreSignedFulcioWithSubjectEmail(subjectEmail string) PRSigstoreSignedFulcioOption { + return func(f *prSigstoreSignedFulcio) error { + if f.SubjectEmail != "" { + return errors.New(`"subjectEmail" already specified`) + } + f.SubjectEmail = subjectEmail + return nil + } +} + +// newPRSigstoreSignedFulcio is NewPRSigstoreSignedFulcio, except it returns the private type +func newPRSigstoreSignedFulcio(options ...PRSigstoreSignedFulcioOption) (*prSigstoreSignedFulcio, error) { + res := prSigstoreSignedFulcio{} + for _, o := range options { + if err := o(&res); err != nil { + return nil, err + } + } + + if res.CAPath != "" && res.CAData != nil { + return nil, InvalidPolicyFormatError("caPath and caData cannot be used simultaneously") + } + if res.CAPath == "" && res.CAData == nil { + return nil, InvalidPolicyFormatError("At least one of caPath and caData must be specified") + } + if res.OIDCIssuer == "" { + return nil, InvalidPolicyFormatError("oidcIssuer not specified") + } + if res.SubjectEmail == "" { + return nil, InvalidPolicyFormatError("subjectEmail not specified") + } + + return &res, nil +} + +// NewPRSigstoreSignedFulcio returns a PRSigstoreSignedFulcio based on options. +func NewPRSigstoreSignedFulcio(options ...PRSigstoreSignedFulcioOption) (PRSigstoreSignedFulcio, error) { + return newPRSigstoreSignedFulcio(options...) +} + +// Compile-time check that prSigstoreSignedFulcio implements json.Unmarshaler. +var _ json.Unmarshaler = (*prSigstoreSignedFulcio)(nil) + +func (f *prSigstoreSignedFulcio) UnmarshalJSON(data []byte) error { + *f = prSigstoreSignedFulcio{} + var tmp prSigstoreSignedFulcio + var gotCAPath, gotCAData, gotOIDCIssuer, gotSubjectEmail bool // = false... + if err := internal.ParanoidUnmarshalJSONObject(data, func(key string) any { + switch key { + case "caPath": + gotCAPath = true + return &tmp.CAPath + case "caData": + gotCAData = true + return &tmp.CAData + case "oidcIssuer": + gotOIDCIssuer = true + return &tmp.OIDCIssuer + case "subjectEmail": + gotSubjectEmail = true + return &tmp.SubjectEmail + default: + return nil + } + }); err != nil { + return err + } + + var opts []PRSigstoreSignedFulcioOption + if gotCAPath { + opts = append(opts, PRSigstoreSignedFulcioWithCAPath(tmp.CAPath)) + } + if gotCAData { + opts = append(opts, PRSigstoreSignedFulcioWithCAData(tmp.CAData)) + } + if gotOIDCIssuer { + opts = append(opts, PRSigstoreSignedFulcioWithOIDCIssuer(tmp.OIDCIssuer)) + } + if gotSubjectEmail { + opts = append(opts, PRSigstoreSignedFulcioWithSubjectEmail(tmp.SubjectEmail)) + } + + res, err := newPRSigstoreSignedFulcio(opts...) + if err != nil { + return err + } + + *f = *res + return nil +} diff --git a/signature/policy_config_sigstore_test.go b/signature/policy_config_sigstore_test.go new file mode 100644 index 0000000000..d71ae5f895 --- /dev/null +++ b/signature/policy_config_sigstore_test.go @@ -0,0 +1,502 @@ +package signature + +import ( + "encoding/json" + "testing" + + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// xNewPRSigstoreSigned is like NewPRSigstoreSigned, except it must not fail. +func xNewPRSigstoreSigned(options ...PRSigstoreSignedOption) PolicyRequirement { + pr, err := NewPRSigstoreSigned(options...) + if err != nil { + panic("xNewPRSigstoreSigned failed") + } + return pr +} + +func TestNewPRSigstoreSigned(t *testing.T) { + const testKeyPath = "/foo/bar" + testKeyData := []byte("abc") + testFulcio, err := NewPRSigstoreSignedFulcio( + PRSigstoreSignedFulcioWithCAPath("fixtures/fulcio_v1.crt.pem"), + PRSigstoreSignedFulcioWithOIDCIssuer("https://github.com/login/oauth"), + PRSigstoreSignedFulcioWithSubjectEmail("mitr@redhat.com"), + ) + require.NoError(t, err) + const testRekorKeyPath = "/foo/baz" + testRekorKeyData := []byte("def") + testIdentity := NewPRMMatchRepoDigestOrExact() + + // Success: combinatoric combinations of key source and Rekor uses + for _, c := range []struct { + options []PRSigstoreSignedOption + requiresRekor bool + expected prSigstoreSigned + }{ + { + options: []PRSigstoreSignedOption{ + PRSigstoreSignedWithKeyPath(testKeyPath), + PRSigstoreSignedWithSignedIdentity(testIdentity), + }, + expected: prSigstoreSigned{ + prCommon: prCommon{prTypeSigstoreSigned}, + KeyPath: testKeyPath, + KeyData: nil, + Fulcio: nil, + SignedIdentity: testIdentity, + }, + }, + { + options: []PRSigstoreSignedOption{ + PRSigstoreSignedWithKeyData(testKeyData), + PRSigstoreSignedWithSignedIdentity(testIdentity), + }, + expected: prSigstoreSigned{ + prCommon: prCommon{prTypeSigstoreSigned}, + KeyPath: "", + KeyData: testKeyData, + Fulcio: nil, + SignedIdentity: testIdentity, + }, + }, + { + options: []PRSigstoreSignedOption{ + PRSigstoreSignedWithFulcio(testFulcio), + PRSigstoreSignedWithSignedIdentity(testIdentity), + }, + requiresRekor: true, + expected: prSigstoreSigned{ + prCommon: prCommon{prTypeSigstoreSigned}, + KeyPath: "", + KeyData: nil, + Fulcio: testFulcio, + SignedIdentity: testIdentity, + }, + }, + } { + for _, c2 := range []struct { + rekorOptions []PRSigstoreSignedOption + rekorExpected prSigstoreSigned + }{ + { // No Rekor + rekorOptions: []PRSigstoreSignedOption{}, + rekorExpected: prSigstoreSigned{}, + }, + { + rekorOptions: []PRSigstoreSignedOption{ + PRSigstoreSignedWithRekorPublicKeyPath(testRekorKeyPath), + }, + rekorExpected: prSigstoreSigned{ + RekorPublicKeyPath: testRekorKeyPath, + }, + }, + { + rekorOptions: []PRSigstoreSignedOption{ + PRSigstoreSignedWithRekorPublicKeyData(testRekorKeyData), + }, + rekorExpected: prSigstoreSigned{ + RekorPublicKeyData: testRekorKeyData, + }, + }, + } { + // Special-case this rejected combination: + if c.requiresRekor && len(c2.rekorOptions) == 0 { + continue + } + pr, err := newPRSigstoreSigned(append(c.options, c2.rekorOptions...)...) + require.NoError(t, err) + expected := c.expected // A shallow copy + expected.RekorPublicKeyPath = c2.rekorExpected.RekorPublicKeyPath + expected.RekorPublicKeyData = c2.rekorExpected.RekorPublicKeyData + assert.Equal(t, &expected, pr) + } + } + + testFulcio2, err := NewPRSigstoreSignedFulcio( + PRSigstoreSignedFulcioWithCAPath("fixtures/fulcio_v1.crt.pem"), + PRSigstoreSignedFulcioWithOIDCIssuer("https://github.com/login/oauth"), + PRSigstoreSignedFulcioWithSubjectEmail("test-user@example.com"), + ) + require.NoError(t, err) + for _, c := range [][]PRSigstoreSignedOption{ + {}, // None of keyPath nor keyData, fulcio specified + { // Both keyPath and keyData specified + PRSigstoreSignedWithKeyPath(testKeyPath), + PRSigstoreSignedWithKeyData(testKeyData), + PRSigstoreSignedWithSignedIdentity(testIdentity), + }, + { // both keyPath and fulcio specified + PRSigstoreSignedWithKeyPath(testKeyPath), + PRSigstoreSignedWithFulcio(testFulcio), + PRSigstoreSignedWithRekorPublicKeyPath(testRekorKeyPath), + PRSigstoreSignedWithSignedIdentity(testIdentity), + }, + { // both keyData and fulcio specified + PRSigstoreSignedWithKeyData(testKeyData), + PRSigstoreSignedWithFulcio(testFulcio), + PRSigstoreSignedWithRekorPublicKeyPath(testRekorKeyPath), + PRSigstoreSignedWithSignedIdentity(testIdentity), + }, + { // Duplicate keyPath + PRSigstoreSignedWithKeyPath(testKeyPath), + PRSigstoreSignedWithKeyPath(testKeyPath + "1"), + PRSigstoreSignedWithSignedIdentity(testIdentity), + }, + { // Duplicate keyData + PRSigstoreSignedWithKeyData(testKeyData), + PRSigstoreSignedWithKeyData([]byte("def")), + PRSigstoreSignedWithSignedIdentity(testIdentity), + }, + { // Duplicate fulcio + PRSigstoreSignedWithFulcio(testFulcio), + PRSigstoreSignedWithFulcio(testFulcio2), + PRSigstoreSignedWithRekorPublicKeyPath(testRekorKeyPath), + PRSigstoreSignedWithSignedIdentity(testIdentity), + }, + { // fulcio without Rekor + PRSigstoreSignedWithFulcio(testFulcio), + PRSigstoreSignedWithSignedIdentity(testIdentity), + }, + { // Both rekorKeyPath and rekorKeyData specified + PRSigstoreSignedWithKeyPath(testKeyPath), + PRSigstoreSignedWithRekorPublicKeyPath(testRekorKeyPath), + PRSigstoreSignedWithRekorPublicKeyData(testRekorKeyData), + PRSigstoreSignedWithSignedIdentity(testIdentity), + }, + { // Duplicate rekorKeyPath + PRSigstoreSignedWithKeyPath(testKeyPath), + PRSigstoreSignedWithRekorPublicKeyPath(testRekorKeyPath), + PRSigstoreSignedWithRekorPublicKeyPath(testRekorKeyPath + "1"), + PRSigstoreSignedWithSignedIdentity(testIdentity), + }, + { // Duplicate keyData + PRSigstoreSignedWithKeyPath(testKeyPath), + PRSigstoreSignedWithRekorPublicKeyData(testRekorKeyData), + PRSigstoreSignedWithRekorPublicKeyData([]byte("def")), + PRSigstoreSignedWithSignedIdentity(testIdentity), + }, + { // Missing signedIdentity + PRSigstoreSignedWithKeyPath(testKeyPath), + }, + { // Duplicate signedIdentity} + PRSigstoreSignedWithKeyPath(testKeyPath), + PRSigstoreSignedWithSignedIdentity(testIdentity), + PRSigstoreSignedWithSignedIdentity(newPRMMatchRepository()), + }, + } { + _, err = newPRSigstoreSigned(c...) + assert.Error(t, err) + } +} + +func TestNewPRSigstoreSignedKeyPath(t *testing.T) { + const testPath = "/foo/bar" + signedIdentity := NewPRMMatchRepoDigestOrExact() + _pr, err := NewPRSigstoreSignedKeyPath(testPath, signedIdentity) + require.NoError(t, err) + pr, ok := _pr.(*prSigstoreSigned) + require.True(t, ok) + assert.Equal(t, &prSigstoreSigned{ + prCommon: prCommon{Type: prTypeSigstoreSigned}, + KeyPath: testPath, + SignedIdentity: NewPRMMatchRepoDigestOrExact(), + }, pr) +} + +func TestNewPRSigstoreSignedKeyData(t *testing.T) { + testData := []byte("abc") + signedIdentity := NewPRMMatchRepoDigestOrExact() + _pr, err := NewPRSigstoreSignedKeyData(testData, signedIdentity) + require.NoError(t, err) + pr, ok := _pr.(*prSigstoreSigned) + require.True(t, ok) + assert.Equal(t, &prSigstoreSigned{ + prCommon: prCommon{Type: prTypeSigstoreSigned}, + KeyData: testData, + SignedIdentity: NewPRMMatchRepoDigestOrExact(), + }, pr) +} + +// Return the result of modifying validJSON with fn and unmarshaling it into *pr +func tryUnmarshalModifiedSigstoreSigned(t *testing.T, pr *prSigstoreSigned, validJSON []byte, modifyFn func(mSA)) error { + var tmp mSA + err := json.Unmarshal(validJSON, &tmp) + require.NoError(t, err) + + modifyFn(tmp) + + *pr = prSigstoreSigned{} + return jsonUnmarshalFromObject(t, tmp, &pr) +} + +func TestPRSigstoreSignedUnmarshalJSON(t *testing.T) { + keyDataTests := policyJSONUmarshallerTests[PolicyRequirement]{ + newDest: func() json.Unmarshaler { return &prSigstoreSigned{} }, + newValidObject: func() (PolicyRequirement, error) { + return NewPRSigstoreSignedKeyData([]byte("abc"), NewPRMMatchRepoDigestOrExact()) + }, + otherJSONParser: newPolicyRequirementFromJSON, + breakFns: []func(mSA){ + // The "type" field is missing + func(v mSA) { delete(v, "type") }, + // Wrong "type" field + func(v mSA) { v["type"] = 1 }, + func(v mSA) { v["type"] = "this is invalid" }, + // Extra top-level sub-object + func(v mSA) { v["unexpected"] = 1 }, + // All of "keyPath" and "keyData", and "fulcio" is missing + func(v mSA) { delete(v, "keyData") }, + // Both "keyPath" and "keyData" is present + func(v mSA) { v["keyPath"] = "/foo/bar" }, + // Both "keyData" and "fulcio" is present + func(v mSA) { + v["fulcio"] = mSA{ + "caPath": "/foo/baz", + "oidcIssuer": "https://example.com", + "subjectEmail": "test@example.com", + } + }, + // Invalid "keyPath" field + func(v mSA) { delete(v, "keyData"); v["keyPath"] = 1 }, + // Invalid "keyData" field + func(v mSA) { v["keyData"] = 1 }, + func(v mSA) { v["keyData"] = "this is invalid base64" }, + // Invalid "fulcio" field + func(v mSA) { v["fulcio"] = 1 }, + func(v mSA) { v["fulcio"] = mSA{} }, + // "fulcio" is explicit nil + func(v mSA) { v["fulcio"] = nil }, + // Both "rekorKeyPath" and "rekorKeyData" is present + func(v mSA) { + v["rekorPublicKeyPath"] = "/foo/baz" + v["rekorPublicKeyData"] = "" + }, + // Invalid "rekorPublicKeyPath" field + func(v mSA) { v["rekorPublicKeyPath"] = 1 }, + // Invalid "rekorPublicKeyData" field + func(v mSA) { v["rekorPublicKeyData"] = 1 }, + func(v mSA) { v["rekorPublicKeyData"] = "this is invalid base64" }, + // Invalid "signedIdentity" field + func(v mSA) { v["signedIdentity"] = "this is invalid" }, + // "signedIdentity" an explicit nil + func(v mSA) { v["signedIdentity"] = nil }, + }, + duplicateFields: []string{"type", "keyData", "signedIdentity"}, + } + keyDataTests.run(t) + // Test keyPath-specific duplicate fields + policyJSONUmarshallerTests[PolicyRequirement]{ + newDest: func() json.Unmarshaler { return &prSigstoreSigned{} }, + newValidObject: func() (PolicyRequirement, error) { + return NewPRSigstoreSignedKeyPath("/foo/bar", NewPRMMatchRepoDigestOrExact()) + }, + otherJSONParser: newPolicyRequirementFromJSON, + duplicateFields: []string{"type", "keyPath", "signedIdentity"}, + }.run(t) + // Test Fulcio and rekorPublicKeyPath duplicate fields + testFulcio, err := NewPRSigstoreSignedFulcio( + PRSigstoreSignedFulcioWithCAPath("fixtures/fulcio_v1.crt.pem"), + PRSigstoreSignedFulcioWithOIDCIssuer("https://github.com/login/oauth"), + PRSigstoreSignedFulcioWithSubjectEmail("mitr@redhat.com"), + ) + require.NoError(t, err) + policyJSONUmarshallerTests[PolicyRequirement]{ + newDest: func() json.Unmarshaler { return &prSigstoreSigned{} }, + newValidObject: func() (PolicyRequirement, error) { + return NewPRSigstoreSigned( + PRSigstoreSignedWithFulcio(testFulcio), + PRSigstoreSignedWithRekorPublicKeyPath("/foo/rekor"), + PRSigstoreSignedWithSignedIdentity(NewPRMMatchRepoDigestOrExact()), + ) + }, + otherJSONParser: newPolicyRequirementFromJSON, + duplicateFields: []string{"type", "fulcio", "rekorPublicKeyPath", "signedIdentity"}, + }.run(t) + // Test rekorPublicKeyData duplicate fields + policyJSONUmarshallerTests[PolicyRequirement]{ + newDest: func() json.Unmarshaler { return &prSigstoreSigned{} }, + newValidObject: func() (PolicyRequirement, error) { + return NewPRSigstoreSigned( + PRSigstoreSignedWithKeyPath("/foo/bar"), + PRSigstoreSignedWithRekorPublicKeyData([]byte("foo")), + PRSigstoreSignedWithSignedIdentity(NewPRMMatchRepoDigestOrExact()), + ) + }, + otherJSONParser: newPolicyRequirementFromJSON, + duplicateFields: []string{"type", "keyPath", "rekorPublicKeyData", "signedIdentity"}, + }.run(t) + + var pr prSigstoreSigned + + // Start with a valid JSON. + _, validJSON := keyDataTests.validObjectAndJSON(t) + + // Various allowed modifications to the requirement + allowedModificationFns := []func(mSA){ + // Delete the signedIdentity field + func(v mSA) { delete(v, "signedIdentity") }, + } + for _, fn := range allowedModificationFns { + err := tryUnmarshalModifiedSigstoreSigned(t, &pr, validJSON, fn) + require.NoError(t, err) + } + + // Various ways to set signedIdentity to the default value + signedIdentityDefaultFns := []func(mSA){ + // Set signedIdentity to the default explicitly + func(v mSA) { v["signedIdentity"] = NewPRMMatchRepoDigestOrExact() }, + // Delete the signedIdentity field + func(v mSA) { delete(v, "signedIdentity") }, + } + for _, fn := range signedIdentityDefaultFns { + err := tryUnmarshalModifiedSigstoreSigned(t, &pr, validJSON, fn) + require.NoError(t, err) + assert.Equal(t, NewPRMMatchRepoDigestOrExact(), pr.SignedIdentity) + } +} + +func TestNewPRSigstoreSignedFulcio(t *testing.T) { + const testCAPath = "/foo/bar" + testCAData := []byte("abc") + const testOIDCIssuer = "https://example.com" + const testSubjectEmail = "test@example.com" + + // Success: + for _, c := range []struct { + options []PRSigstoreSignedFulcioOption + expected prSigstoreSignedFulcio + }{ + { + options: []PRSigstoreSignedFulcioOption{ + PRSigstoreSignedFulcioWithCAPath(testCAPath), + PRSigstoreSignedFulcioWithOIDCIssuer(testOIDCIssuer), + PRSigstoreSignedFulcioWithSubjectEmail(testSubjectEmail), + }, + expected: prSigstoreSignedFulcio{ + CAPath: testCAPath, + OIDCIssuer: testOIDCIssuer, + SubjectEmail: testSubjectEmail, + }, + }, + { + options: []PRSigstoreSignedFulcioOption{ + PRSigstoreSignedFulcioWithCAData(testCAData), + PRSigstoreSignedFulcioWithOIDCIssuer(testOIDCIssuer), + PRSigstoreSignedFulcioWithSubjectEmail(testSubjectEmail), + }, + expected: prSigstoreSignedFulcio{ + CAData: testCAData, + OIDCIssuer: testOIDCIssuer, + SubjectEmail: testSubjectEmail, + }, + }, + } { + pr, err := newPRSigstoreSignedFulcio(c.options...) + require.NoError(t, err) + assert.Equal(t, &c.expected, pr) + } + + for _, c := range [][]PRSigstoreSignedFulcioOption{ + { // Neither caPath nor caData specified + PRSigstoreSignedFulcioWithOIDCIssuer(testOIDCIssuer), + PRSigstoreSignedFulcioWithSubjectEmail(testSubjectEmail), + }, + { // Both caPath and caData specified + PRSigstoreSignedFulcioWithCAPath(testCAPath), + PRSigstoreSignedFulcioWithCAData(testCAData), + PRSigstoreSignedFulcioWithOIDCIssuer(testOIDCIssuer), + PRSigstoreSignedFulcioWithSubjectEmail(testSubjectEmail), + }, + { // Duplicate caPath + PRSigstoreSignedFulcioWithCAPath(testCAPath), + PRSigstoreSignedFulcioWithCAPath(testCAPath + "1"), + PRSigstoreSignedFulcioWithOIDCIssuer(testOIDCIssuer), + PRSigstoreSignedFulcioWithSubjectEmail(testSubjectEmail), + }, + { // Duplicate caData + PRSigstoreSignedFulcioWithCAData(testCAData), + PRSigstoreSignedFulcioWithCAData([]byte("def")), + PRSigstoreSignedFulcioWithOIDCIssuer(testOIDCIssuer), + PRSigstoreSignedFulcioWithSubjectEmail(testSubjectEmail), + }, + { // Missing oidcIssuer + PRSigstoreSignedFulcioWithCAPath(testCAPath), + PRSigstoreSignedFulcioWithSubjectEmail(testSubjectEmail), + }, + { // Duplicate oidcIssuer + PRSigstoreSignedFulcioWithCAPath(testCAPath), + PRSigstoreSignedFulcioWithOIDCIssuer(testOIDCIssuer), + PRSigstoreSignedFulcioWithOIDCIssuer(testOIDCIssuer + "1"), + PRSigstoreSignedFulcioWithSubjectEmail(testSubjectEmail), + }, + { // Missing subjectEmail + PRSigstoreSignedFulcioWithCAPath(testCAPath), + PRSigstoreSignedFulcioWithOIDCIssuer(testOIDCIssuer), + }, + { // Duplicate subjectEmail + PRSigstoreSignedFulcioWithCAPath(testCAPath), + PRSigstoreSignedFulcioWithOIDCIssuer(testOIDCIssuer), + PRSigstoreSignedFulcioWithSubjectEmail(testSubjectEmail), + PRSigstoreSignedFulcioWithSubjectEmail("1" + testSubjectEmail), + }, + } { + _, err := newPRSigstoreSignedFulcio(c...) + logrus.Errorf("%#v", err) + assert.Error(t, err) + } +} + +func TestPRSigstoreSignedFulcioUnmarshalJSON(t *testing.T) { + policyJSONUmarshallerTests[PRSigstoreSignedFulcio]{ + newDest: func() json.Unmarshaler { return &prSigstoreSignedFulcio{} }, + newValidObject: func() (PRSigstoreSignedFulcio, error) { + return NewPRSigstoreSignedFulcio( + PRSigstoreSignedFulcioWithCAPath("fixtures/fulcio_v1.crt.pem"), + PRSigstoreSignedFulcioWithOIDCIssuer("https://github.com/login/oauth"), + PRSigstoreSignedFulcioWithSubjectEmail("mitr@redhat.com"), + ) + }, + otherJSONParser: nil, + breakFns: []func(mSA){ + // Extra top-level sub-object + func(v mSA) { v["unexpected"] = 1 }, + // Both of "caPath" and "caData" are missing + func(v mSA) { delete(v, "caPath") }, + // Both "caPath" and "caData" is present + func(v mSA) { v["caData"] = "" }, + // Invalid "caPath" field + func(v mSA) { v["caPath"] = 1 }, + // Invalid "oidcIssuer" field + func(v mSA) { v["oidcIssuer"] = 1 }, + // "oidcIssuer" is missing + func(v mSA) { delete(v, "oidcIssuer") }, + // Invalid "subjectEmail" field + func(v mSA) { v["subjectEmail"] = 1 }, + // "subjectEmail" is missing + func(v mSA) { delete(v, "subjectEmail") }, + }, + duplicateFields: []string{"caPath", "oidcIssuer", "subjectEmail"}, + }.run(t) + // Test caData specifics + policyJSONUmarshallerTests[PRSigstoreSignedFulcio]{ + newDest: func() json.Unmarshaler { return &prSigstoreSignedFulcio{} }, + newValidObject: func() (PRSigstoreSignedFulcio, error) { + return NewPRSigstoreSignedFulcio( + PRSigstoreSignedFulcioWithCAData([]byte("abc")), + PRSigstoreSignedFulcioWithOIDCIssuer("https://github.com/login/oauth"), + PRSigstoreSignedFulcioWithSubjectEmail("mitr@redhat.com"), + ) + }, + otherJSONParser: nil, + breakFns: []func(mSA){ + // Invalid "caData" field + func(v mSA) { v["caData"] = 1 }, + func(v mSA) { v["caData"] = "this is invalid base64" }, + }, + duplicateFields: []string{"caData", "oidcIssuer", "subjectEmail"}, + }.run(t) +} diff --git a/signature/policy_config_test.go b/signature/policy_config_test.go index 19d6a21547..1af6dc4604 100644 --- a/signature/policy_config_test.go +++ b/signature/policy_config_test.go @@ -10,6 +10,7 @@ import ( "github.com/containers/image/v5/directory" "github.com/containers/image/v5/docker" + "golang.org/x/exp/maps" // this import is needed where we use the "atomic" transport in TestPolicyUnmarshalJSON _ "github.com/containers/image/v5/openshift" @@ -18,16 +19,16 @@ import ( "github.com/stretchr/testify/require" ) -type mSI map[string]interface{} // To minimize typing the long name +type mSA map[string]any // To minimize typing the long name // A short-hand way to get a JSON object field value or panic. No error handling done, we know // what we are working with, a panic in a test is good enough, and fitting test cases on a single line // is a priority. -func x(m mSI, fields ...string) mSI { +func x(m mSA, fields ...string) mSA { for _, field := range fields { - // Not .(mSI) because type assertion of an unnamed type to a named type always fails (the types + // Not .(mSA) because type assertion of an unnamed type to a named type always fails (the types // are not "identical"), but the assignment is fine because they are "assignable". - m = m[field].(map[string]interface{}) + m = m[field].(map[string]any) } return m } @@ -104,12 +105,16 @@ var policyFixtureContents = &Policy{ xNewPRSignedBaseLayer(xNewPRMExactReference("registry.access.redhat.com/rhel7/rhel:latest")), }, "example.com/sigstore/key-data-example": { - xNewPRSigstoreSignedKeyData([]byte("nonsense"), - NewPRMMatchRepoDigestOrExact()), + xNewPRSigstoreSigned( + PRSigstoreSignedWithKeyData([]byte("nonsense")), + PRSigstoreSignedWithSignedIdentity(NewPRMMatchRepoDigestOrExact()), + ), }, "example.com/sigstore/key-path-example": { - xNewPRSigstoreSignedKeyPath("/keys/public-key", - NewPRMMatchRepository()), + xNewPRSigstoreSigned( + PRSigstoreSignedWithKeyPath("/keys/public-key"), + PRSigstoreSignedWithSignedIdentity(NewPRMMatchRepository()), + ), }, }, }, @@ -256,7 +261,7 @@ func TestNewPolicyFromBytes(t *testing.T) { // jsonUnmarshalFromObject is like json.Unmarshal(), but the input is an arbitrary object // that is JSON-marshalled first (as a convenient way to create an invalid/unusual JSON input) -func jsonUnmarshalFromObject(t *testing.T, object interface{}, dest interface{}) error { +func jsonUnmarshalFromObject(t *testing.T, object any, dest any) error { testJSON, err := json.Marshal(object) require.NoError(t, err) return json.Unmarshal(testJSON, dest) @@ -264,8 +269,8 @@ func jsonUnmarshalFromObject(t *testing.T, object interface{}, dest interface{}) // assertJSONUnmarshalFromObjectFails checks that unmarshaling the JSON-marshaled version // of an arbitrary object (as a convenient way to create an invalid/unusual JSON input) into -// into dest fails. -func assertJSONUnmarshalFromObjectFails(t *testing.T, object interface{}, dest interface{}) { +// dest fails. +func assertJSONUnmarshalFromObjectFails(t *testing.T, object any, dest any) { err := jsonUnmarshalFromObject(t, object, dest) assert.Error(t, err) } @@ -287,7 +292,7 @@ func testInvalidJSONInput(t *testing.T, dest json.Unmarshaler) { // addExtraJSONMember adds an additional member "$name": $extra, // possibly with a duplicate name, to encoded. // Errors, if any, are reported through t. -func addExtraJSONMember(t *testing.T, encoded []byte, name string, extra interface{}) []byte { +func addExtraJSONMember(t *testing.T, encoded []byte, name string, extra any) []byte { extraJSON, err := json.Marshal(extra) require.NoError(t, err) @@ -297,7 +302,7 @@ func addExtraJSONMember(t *testing.T, encoded []byte, name string, extra interfa res := bytes.Join([][]byte{encoded[:preservedLen], []byte(`,"`), []byte(name), []byte(`":`), extraJSON, []byte("}")}, nil) // Verify that the result is valid JSON, as a sanity check that we are actually triggering // the “duplicate member” case in the caller. - var raw map[string]interface{} + var raw map[string]any err = json.Unmarshal(res, &raw) require.NoError(t, err) return res @@ -305,17 +310,17 @@ func addExtraJSONMember(t *testing.T, encoded []byte, name string, extra interfa // policyJSONUnmarshallerTests formalizes the repeated structure of the JSON unmarshaller // tests in this file, and allows sharing the test implementation. -type policyJSONUmarshallerTests struct { - newDest func() json.Unmarshaler // Create a new json.Unmarshaler to test against - newValidObject func() (interface{}, error) // A function that generates a valid object, used as a base for other tests - otherJSONParser func([]byte) (interface{}, error) // Another function that must accept the result of encoding validObject - invalidObjects []mSI // mSI values that are invalid for this unmarshaller; a simpler alternative to breakFns - breakFns []func(mSI) // Functions that edit a mSI from newValidObject() to make it invalid - duplicateFields []string // Names of fields in the return value of newValidObject() that should not be duplicated +type policyJSONUmarshallerTests[T any] struct { + newDest func() json.Unmarshaler // Create a new json.Unmarshaler to test against + newValidObject func() (T, error) // A function that generates a valid object, used as a base for other tests + otherJSONParser func([]byte) (T, error) // Another function that must accept the result of encoding validObject + invalidObjects []mSA // mSA values that are invalid for this unmarshaller; a simpler alternative to breakFns + breakFns []func(mSA) // Functions that edit a mSA from newValidObject() to make it invalid + duplicateFields []string // Names of fields in the return value of newValidObject() that should not be duplicated } // validObjectAndJSON returns an object created by d.newValidObject() and its JSON representation. -func (d policyJSONUmarshallerTests) validObjectAndJSON(t *testing.T) (interface{}, []byte) { +func (d policyJSONUmarshallerTests[T]) validObjectAndJSON(t *testing.T) (T, []byte) { validObject, err := d.newValidObject() require.NoError(t, err) validJSON, err := json.Marshal(validObject) @@ -323,7 +328,7 @@ func (d policyJSONUmarshallerTests) validObjectAndJSON(t *testing.T) (interface{ return validObject, validJSON } -func (d policyJSONUmarshallerTests) run(t *testing.T) { +func (d policyJSONUmarshallerTests[T]) run(t *testing.T) { dest := d.newDest() testInvalidJSONInput(t, dest) @@ -350,7 +355,7 @@ func (d policyJSONUmarshallerTests) run(t *testing.T) { // Various ways to corrupt the JSON for index, fn := range d.breakFns { t.Run(fmt.Sprintf("breakFns[%d]", index), func(t *testing.T) { - var tmp mSI + var tmp mSA err := json.Unmarshal(validJSON, &tmp) require.NoError(t, err) @@ -363,7 +368,7 @@ func (d policyJSONUmarshallerTests) run(t *testing.T) { // Duplicated fields for _, field := range d.duplicateFields { - var tmp mSI + var tmp mSA err := json.Unmarshal(validJSON, &tmp) require.NoError(t, err) @@ -403,9 +408,9 @@ func xNewPRSignedByKeyData(keyType sbKeyType, keyData []byte, signedIdentity Pol } func TestPolicyUnmarshalJSON(t *testing.T) { - tests := policyJSONUmarshallerTests{ + tests := policyJSONUmarshallerTests[*Policy]{ newDest: func() json.Unmarshaler { return &Policy{} }, - newValidObject: func() (interface{}, error) { + newValidObject: func() (*Policy, error) { return &Policy{ Default: []PolicyRequirement{ xNewPRSignedByKeyData(SBKeyTypeGPGKeys, []byte("abc"), NewPRMMatchRepoDigestOrExact()), @@ -433,19 +438,19 @@ func TestPolicyUnmarshalJSON(t *testing.T) { }, nil }, otherJSONParser: nil, - breakFns: []func(mSI){ + breakFns: []func(mSA){ // The "default" field is missing - func(v mSI) { delete(v, "default") }, + func(v mSA) { delete(v, "default") }, // Extra top-level sub-object - func(v mSI) { v["unexpected"] = 1 }, + func(v mSA) { v["unexpected"] = 1 }, // "default" not an array - func(v mSI) { v["default"] = 1 }, - func(v mSI) { v["default"] = mSI{} }, + func(v mSA) { v["default"] = 1 }, + func(v mSA) { v["default"] = mSA{} }, // "transports" not an object - func(v mSI) { v["transports"] = 1 }, - func(v mSI) { v["transports"] = []string{} }, + func(v mSA) { v["transports"] = 1 }, + func(v mSA) { v["transports"] = []string{} }, // "default" is an invalid PolicyRequirements - func(v mSI) { v["default"] = PolicyRequirements{} }, + func(v mSA) { v["default"] = PolicyRequirements{} }, }, duplicateFields: []string{"default", "transports"}, } @@ -453,14 +458,14 @@ func TestPolicyUnmarshalJSON(t *testing.T) { // Various allowed modifications to the policy _, validJSON := tests.validObjectAndJSON(t) - allowedModificationFns := []func(mSI){ + allowedModificationFns := []func(mSA){ // Delete the map of transport-specific scopes - func(v mSI) { delete(v, "transports") }, + func(v mSA) { delete(v, "transports") }, // Use an empty map of transport-specific scopes - func(v mSI) { v["transports"] = map[string]PolicyTransportScopes{} }, + func(v mSA) { v["transports"] = map[string]PolicyTransportScopes{} }, } for _, fn := range allowedModificationFns { - var tmp mSI + var tmp mSA err := json.Unmarshal(validJSON, &tmp) require.NoError(t, err) @@ -488,8 +493,8 @@ func TestPolicyTransportScopesUnmarshalJSON(t *testing.T) { // Return the result of modifying validJSON with fn and unmarshaling it into *pts // using transport. func tryUnmarshalModifiedPTS(t *testing.T, pts *PolicyTransportScopes, transport types.ImageTransport, - validJSON []byte, modifyFn func(mSI)) error { - var tmp mSI + validJSON []byte, modifyFn func(mSA)) error { + var tmp mSA err := json.Unmarshal(validJSON, &tmp) require.NoError(t, err) @@ -538,15 +543,15 @@ func TestPolicyTransportScopesWithTransportUnmarshalJSON(t *testing.T) { assert.Equal(t, validPTS, pts) // Various ways to corrupt the JSON - breakFns := []func(mSI){ + breakFns := []func(mSA){ // A scope is not an array - func(v mSI) { v["docker.io/library/busybox"] = 1 }, - func(v mSI) { v["docker.io/library/busybox"] = mSI{} }, - func(v mSI) { v[""] = 1 }, - func(v mSI) { v[""] = mSI{} }, + func(v mSA) { v["docker.io/library/busybox"] = 1 }, + func(v mSA) { v["docker.io/library/busybox"] = mSA{} }, + func(v mSA) { v[""] = 1 }, + func(v mSA) { v[""] = mSA{} }, // A scope is an invalid PolicyRequirements - func(v mSI) { v["docker.io/library/busybox"] = PolicyRequirements{} }, - func(v mSI) { v[""] = PolicyRequirements{} }, + func(v mSA) { v["docker.io/library/busybox"] = PolicyRequirements{} }, + func(v mSA) { v[""] = PolicyRequirements{} }, } for _, fn := range breakFns { err = tryUnmarshalModifiedPTS(t, &pts, docker.Transport, validJSON, fn) @@ -555,7 +560,7 @@ func TestPolicyTransportScopesWithTransportUnmarshalJSON(t *testing.T) { // Duplicated fields for _, field := range []string{"docker.io/library/busybox", ""} { - var tmp mSI + var tmp mSA err := json.Unmarshal(validJSON, &tmp) require.NoError(t, err) @@ -573,19 +578,15 @@ func TestPolicyTransportScopesWithTransportUnmarshalJSON(t *testing.T) { // Scope rejected by transport the Docker scopes we use as valid are rejected by directory.Transport // as relative paths. err = tryUnmarshalModifiedPTS(t, &pts, directory.Transport, validJSON, - func(v mSI) {}) + func(v mSA) {}) assert.Error(t, err) // Various allowed modifications to the policy - allowedModificationFns := []func(mSI){ + allowedModificationFns := []func(mSA){ // The "" scope is missing - func(v mSI) { delete(v, "") }, + func(v mSA) { delete(v, "") }, // The policy is completely empty - func(v mSI) { - for key := range v { - delete(v, key) - } - }, + func(v mSA) { maps.Clear(v) }, } for _, fn := range allowedModificationFns { err = tryUnmarshalModifiedPTS(t, &pts, docker.Transport, validJSON, fn) @@ -594,9 +595,9 @@ func TestPolicyTransportScopesWithTransportUnmarshalJSON(t *testing.T) { } func TestPolicyRequirementsUnmarshalJSON(t *testing.T) { - policyJSONUmarshallerTests{ + policyJSONUmarshallerTests[*PolicyRequirements]{ newDest: func() json.Unmarshaler { return &PolicyRequirements{} }, - newValidObject: func() (interface{}, error) { + newValidObject: func() (*PolicyRequirements, error) { return &PolicyRequirements{ xNewPRSignedByKeyData(SBKeyTypeGPGKeys, []byte("def"), NewPRMMatchRepoDigestOrExact()), xNewPRSignedByKeyData(SBKeyTypeSignedByGPGKeys, []byte("RH"), NewPRMMatchRepository()), @@ -606,8 +607,8 @@ func TestPolicyRequirementsUnmarshalJSON(t *testing.T) { }.run(t) // This would be inconvenient to integrate into policyJSONUnmarshallerTests.invalidObjects - // because all other users are easier to express as mSI. - for _, invalid := range [][]interface{}{ + // because all other users are easier to express as mSA. + for _, invalid := range [][]any{ // No requirements {}, // A member is not an object @@ -635,7 +636,7 @@ func TestNewPolicyRequirementFromJSON(t *testing.T) { assert.Equal(t, validReq, req) // Invalid - for _, invalid := range []interface{}{ + for _, invalid := range []any{ // Not an object 1, // Missing type @@ -664,15 +665,13 @@ func TestNewPRInsecureAcceptAnything(t *testing.T) { } func TestPRInsecureAcceptAnythingUnmarshalJSON(t *testing.T) { - policyJSONUmarshallerTests{ + policyJSONUmarshallerTests[PolicyRequirement]{ newDest: func() json.Unmarshaler { return &prInsecureAcceptAnything{} }, - newValidObject: func() (interface{}, error) { + newValidObject: func() (PolicyRequirement, error) { return NewPRInsecureAcceptAnything(), nil }, - otherJSONParser: func(validJSON []byte) (interface{}, error) { - return newPolicyRequirementFromJSON(validJSON) - }, - invalidObjects: []mSI{ + otherJSONParser: newPolicyRequirementFromJSON, + invalidObjects: []mSA{ // Missing "type" field {}, // Wrong "type" field @@ -696,15 +695,13 @@ func TestNewPRReject(t *testing.T) { } func TestPRRejectUnmarshalJSON(t *testing.T) { - policyJSONUmarshallerTests{ + policyJSONUmarshallerTests[PolicyRequirement]{ newDest: func() json.Unmarshaler { return &prReject{} }, - newValidObject: func() (interface{}, error) { + newValidObject: func() (PolicyRequirement, error) { return NewPRReject(), nil }, - otherJSONParser: func(validJSON []byte) (interface{}, error) { - return newPolicyRequirementFromJSON(validJSON) - }, - invalidObjects: []mSI{ + otherJSONParser: newPolicyRequirementFromJSON, + invalidObjects: []mSA{ // Missing "type" field {}, // Wrong "type" field @@ -812,8 +809,8 @@ func TestNewPRSignedByKeyData(t *testing.T) { } // Return the result of modifying validJSON with fn and unmarshaling it into *pr -func tryUnmarshalModifiedSignedBy(t *testing.T, pr *prSignedBy, validJSON []byte, modifyFn func(mSI)) error { - var tmp mSI +func tryUnmarshalModifiedSignedBy(t *testing.T, pr *prSignedBy, validJSON []byte, modifyFn func(mSA)) error { + var tmp mSA err := json.Unmarshal(validJSON, &tmp) require.NoError(t, err) @@ -824,70 +821,64 @@ func tryUnmarshalModifiedSignedBy(t *testing.T, pr *prSignedBy, validJSON []byte } func TestPRSignedByUnmarshalJSON(t *testing.T) { - keyDataTests := policyJSONUmarshallerTests{ + keyDataTests := policyJSONUmarshallerTests[PolicyRequirement]{ newDest: func() json.Unmarshaler { return &prSignedBy{} }, - newValidObject: func() (interface{}, error) { + newValidObject: func() (PolicyRequirement, error) { return NewPRSignedByKeyData(SBKeyTypeGPGKeys, []byte("abc"), NewPRMMatchRepoDigestOrExact()) }, - otherJSONParser: func(validJSON []byte) (interface{}, error) { - return newPolicyRequirementFromJSON(validJSON) - }, - breakFns: []func(mSI){ + otherJSONParser: newPolicyRequirementFromJSON, + breakFns: []func(mSA){ // The "type" field is missing - func(v mSI) { delete(v, "type") }, + func(v mSA) { delete(v, "type") }, // Wrong "type" field - func(v mSI) { v["type"] = 1 }, - func(v mSI) { v["type"] = "this is invalid" }, + func(v mSA) { v["type"] = 1 }, + func(v mSA) { v["type"] = "this is invalid" }, // Extra top-level sub-object - func(v mSI) { v["unexpected"] = 1 }, + func(v mSA) { v["unexpected"] = 1 }, // The "keyType" field is missing - func(v mSI) { delete(v, "keyType") }, + func(v mSA) { delete(v, "keyType") }, // Invalid "keyType" field - func(v mSI) { v["keyType"] = "this is invalid" }, + func(v mSA) { v["keyType"] = "this is invalid" }, // All three of "keyPath", "keyPaths" and "keyData" are missing - func(v mSI) { delete(v, "keyData") }, + func(v mSA) { delete(v, "keyData") }, // All three of "keyPath", "keyPaths" and "keyData" are present - func(v mSI) { v["keyPath"] = "/foo/bar"; v["keyPaths"] = []string{"/1", "/2"} }, + func(v mSA) { v["keyPath"] = "/foo/bar"; v["keyPaths"] = []string{"/1", "/2"} }, // Two of "keyPath", "keyPaths" and "keyData" are present - func(v mSI) { v["keyPath"] = "/foo/bar"; v["keyPaths"] = []string{"/1", "/2"}; delete(v, "keyData") }, - func(v mSI) { v["keyPath"] = "/foo/bar" }, - func(v mSI) { v["keyPaths"] = []string{"/1", "/2"} }, + func(v mSA) { v["keyPath"] = "/foo/bar"; v["keyPaths"] = []string{"/1", "/2"}; delete(v, "keyData") }, + func(v mSA) { v["keyPath"] = "/foo/bar" }, + func(v mSA) { v["keyPaths"] = []string{"/1", "/2"} }, // Invalid "keyPath" field - func(v mSI) { delete(v, "keyData"); v["keyPath"] = 1 }, + func(v mSA) { delete(v, "keyData"); v["keyPath"] = 1 }, // Invalid "keyPaths" field - func(v mSI) { delete(v, "keyData"); v["keyPaths"] = 1 }, - func(v mSI) { delete(v, "keyData"); v["keyPaths"] = []int{1} }, + func(v mSA) { delete(v, "keyData"); v["keyPaths"] = 1 }, + func(v mSA) { delete(v, "keyData"); v["keyPaths"] = []int{1} }, // Invalid "keyData" field - func(v mSI) { v["keyData"] = 1 }, - func(v mSI) { v["keyData"] = "this is invalid base64" }, + func(v mSA) { v["keyData"] = 1 }, + func(v mSA) { v["keyData"] = "this is invalid base64" }, // Invalid "signedIdentity" field - func(v mSI) { v["signedIdentity"] = "this is invalid" }, + func(v mSA) { v["signedIdentity"] = "this is invalid" }, // "signedIdentity" an explicit nil - func(v mSI) { v["signedIdentity"] = nil }, + func(v mSA) { v["signedIdentity"] = nil }, }, duplicateFields: []string{"type", "keyType", "keyData", "signedIdentity"}, } keyDataTests.run(t) // Test the keyPath-specific aspects - policyJSONUmarshallerTests{ + policyJSONUmarshallerTests[PolicyRequirement]{ newDest: func() json.Unmarshaler { return &prSignedBy{} }, - newValidObject: func() (interface{}, error) { + newValidObject: func() (PolicyRequirement, error) { return NewPRSignedByKeyPath(SBKeyTypeGPGKeys, "/foo/bar", NewPRMMatchRepoDigestOrExact()) }, - otherJSONParser: func(validJSON []byte) (interface{}, error) { - return newPolicyRequirementFromJSON(validJSON) - }, + otherJSONParser: newPolicyRequirementFromJSON, duplicateFields: []string{"type", "keyType", "keyPath", "signedIdentity"}, }.run(t) // Test the keyPaths-specific aspects - policyJSONUmarshallerTests{ + policyJSONUmarshallerTests[PolicyRequirement]{ newDest: func() json.Unmarshaler { return &prSignedBy{} }, - newValidObject: func() (interface{}, error) { + newValidObject: func() (PolicyRequirement, error) { return NewPRSignedByKeyPaths(SBKeyTypeGPGKeys, []string{"/1", "/2"}, NewPRMMatchRepoDigestOrExact()) }, - otherJSONParser: func(validJSON []byte) (interface{}, error) { - return newPolicyRequirementFromJSON(validJSON) - }, + otherJSONParser: newPolicyRequirementFromJSON, duplicateFields: []string{"type", "keyType", "keyPaths", "signedIdentity"}, }.run(t) @@ -897,9 +888,9 @@ func TestPRSignedByUnmarshalJSON(t *testing.T) { _, validJSON := keyDataTests.validObjectAndJSON(t) // Various allowed modifications to the requirement - allowedModificationFns := []func(mSI){ + allowedModificationFns := []func(mSA){ // Delete the signedIdentity field - func(v mSI) { delete(v, "signedIdentity") }, + func(v mSA) { delete(v, "signedIdentity") }, } for _, fn := range allowedModificationFns { err := tryUnmarshalModifiedSignedBy(t, &pr, validJSON, fn) @@ -907,11 +898,11 @@ func TestPRSignedByUnmarshalJSON(t *testing.T) { } // Various ways to set signedIdentity to the default value - signedIdentityDefaultFns := []func(mSI){ + signedIdentityDefaultFns := []func(mSA){ // Set signedIdentity to the default explicitly - func(v mSI) { v["signedIdentity"] = NewPRMMatchRepoDigestOrExact() }, + func(v mSA) { v["signedIdentity"] = NewPRMMatchRepoDigestOrExact() }, // Delete the signedIdentity field - func(v mSI) { delete(v, "signedIdentity") }, + func(v mSA) { delete(v, "signedIdentity") }, } for _, fn := range signedIdentityDefaultFns { err := tryUnmarshalModifiedSignedBy(t, &pr, validJSON, fn) @@ -992,193 +983,33 @@ func TestNewPRSignedBaseLayer(t *testing.T) { } func TestPRSignedBaseLayerUnmarshalJSON(t *testing.T) { - policyJSONUmarshallerTests{ + policyJSONUmarshallerTests[PolicyRequirement]{ newDest: func() json.Unmarshaler { return &prSignedBaseLayer{} }, - newValidObject: func() (interface{}, error) { + newValidObject: func() (PolicyRequirement, error) { baseIdentity, err := NewPRMExactReference("registry.access.redhat.com/rhel7/rhel:7.2.3") require.NoError(t, err) return NewPRSignedBaseLayer(baseIdentity) }, - otherJSONParser: func(validJSON []byte) (interface{}, error) { - return newPolicyRequirementFromJSON(validJSON) - }, - breakFns: []func(mSI){ + otherJSONParser: newPolicyRequirementFromJSON, + breakFns: []func(mSA){ // The "type" field is missing - func(v mSI) { delete(v, "type") }, + func(v mSA) { delete(v, "type") }, // Wrong "type" field - func(v mSI) { v["type"] = 1 }, - func(v mSI) { v["type"] = "this is invalid" }, + func(v mSA) { v["type"] = 1 }, + func(v mSA) { v["type"] = "this is invalid" }, // Extra top-level sub-object - func(v mSI) { v["unexpected"] = 1 }, + func(v mSA) { v["unexpected"] = 1 }, // The "baseLayerIdentity" field is missing - func(v mSI) { delete(v, "baseLayerIdentity") }, + func(v mSA) { delete(v, "baseLayerIdentity") }, // Invalid "baseLayerIdentity" field - func(v mSI) { v["baseLayerIdentity"] = "this is invalid" }, + func(v mSA) { v["baseLayerIdentity"] = "this is invalid" }, // Invalid "baseLayerIdentity" an explicit nil - func(v mSI) { v["baseLayerIdentity"] = nil }, + func(v mSA) { v["baseLayerIdentity"] = nil }, }, duplicateFields: []string{"type", "baseLayerIdentity"}, }.run(t) } -// xNewPRSigstoreSignedKeyPath is like NewPRSigstoreSignedKeyPath, except it must not fail. -func xNewPRSigstoreSignedKeyPath(keyPath string, signedIdentity PolicyReferenceMatch) PolicyRequirement { - pr, err := NewPRSigstoreSignedKeyPath(keyPath, signedIdentity) - if err != nil { - panic("xNewPRSigstoreSignedKeyPath failed") - } - return pr -} - -// xNewPRSigstoreSignedKeyData is like NewPRSigstoreSignedKeyData, except it must not fail. -func xNewPRSigstoreSignedKeyData(keyData []byte, signedIdentity PolicyReferenceMatch) PolicyRequirement { - pr, err := NewPRSigstoreSignedKeyData(keyData, signedIdentity) - if err != nil { - panic("xNewPRSigstoreSignedKeyData failed") - } - return pr -} - -func TestNewPRSigstoreSigned(t *testing.T) { - const testPath = "/foo/bar" - testData := []byte("abc") - testIdentity := NewPRMMatchRepoDigestOrExact() - - // Success - pr, err := newPRSigstoreSigned(testPath, nil, testIdentity) - require.NoError(t, err) - assert.Equal(t, &prSigstoreSigned{ - prCommon: prCommon{prTypeSigstoreSigned}, - KeyPath: testPath, - KeyData: nil, - SignedIdentity: testIdentity, - }, pr) - pr, err = newPRSigstoreSigned("", testData, testIdentity) - require.NoError(t, err) - assert.Equal(t, &prSigstoreSigned{ - prCommon: prCommon{prTypeSigstoreSigned}, - KeyPath: "", - KeyData: testData, - SignedIdentity: testIdentity, - }, pr) - - // Both keyPath and keyData specified - _, err = newPRSigstoreSigned(testPath, testData, testIdentity) - assert.Error(t, err) - - // Invalid signedIdentity - _, err = newPRSigstoreSigned(testPath, nil, nil) - assert.Error(t, err) -} - -func TestNewPRSigstoreSignedKeyPath(t *testing.T) { - const testPath = "/foo/bar" - _pr, err := NewPRSigstoreSignedKeyPath(testPath, NewPRMMatchRepoDigestOrExact()) - require.NoError(t, err) - pr, ok := _pr.(*prSigstoreSigned) - require.True(t, ok) - assert.Equal(t, testPath, pr.KeyPath) - // Failure cases tested in TestNewPRSigstoreSigned. -} - -func TestNewPRSigstoreSignedKeyData(t *testing.T) { - testData := []byte("abc") - _pr, err := NewPRSigstoreSignedKeyData(testData, NewPRMMatchRepoDigestOrExact()) - require.NoError(t, err) - pr, ok := _pr.(*prSigstoreSigned) - require.True(t, ok) - assert.Equal(t, testData, pr.KeyData) - // Failure cases tested in TestNewPRSigstoreSigned. -} - -// Return the result of modifying validJSON with fn and unmarshaling it into *pr -func tryUnmarshalModifiedSigstoreSigned(t *testing.T, pr *prSigstoreSigned, validJSON []byte, modifyFn func(mSI)) error { - var tmp mSI - err := json.Unmarshal(validJSON, &tmp) - require.NoError(t, err) - - modifyFn(tmp) - - *pr = prSigstoreSigned{} - return jsonUnmarshalFromObject(t, tmp, &pr) -} - -func TestPRSigstoreSignedUnmarshalJSON(t *testing.T) { - keyDataTests := policyJSONUmarshallerTests{ - newDest: func() json.Unmarshaler { return &prSigstoreSigned{} }, - newValidObject: func() (interface{}, error) { - return NewPRSigstoreSignedKeyData([]byte("abc"), NewPRMMatchRepoDigestOrExact()) - }, - otherJSONParser: func(validJSON []byte) (interface{}, error) { - return newPolicyRequirementFromJSON(validJSON) - }, - breakFns: []func(mSI){ - // The "type" field is missing - func(v mSI) { delete(v, "type") }, - // Wrong "type" field - func(v mSI) { v["type"] = 1 }, - func(v mSI) { v["type"] = "this is invalid" }, - // Extra top-level sub-object - func(v mSI) { v["unexpected"] = 1 }, - // Both "keyPath" and "keyData" is missing - func(v mSI) { delete(v, "keyData") }, - // Both "keyPath" and "keyData" is present - func(v mSI) { v["keyPath"] = "/foo/bar" }, - // Invalid "keyPath" field - func(v mSI) { delete(v, "keyData"); v["keyPath"] = 1 }, - func(v mSI) { v["type"] = "this is invalid" }, - // Invalid "keyData" field - func(v mSI) { v["keyData"] = 1 }, - func(v mSI) { v["keyData"] = "this is invalid base64" }, - // Invalid "signedIdentity" field - func(v mSI) { v["signedIdentity"] = "this is invalid" }, - // "signedIdentity" an explicit nil - func(v mSI) { v["signedIdentity"] = nil }, - }, - duplicateFields: []string{"type", "keyData", "signedIdentity"}, - } - keyDataTests.run(t) - // Test the keyPath-specific aspects - policyJSONUmarshallerTests{ - newDest: func() json.Unmarshaler { return &prSigstoreSigned{} }, - newValidObject: func() (interface{}, error) { - return NewPRSigstoreSignedKeyPath("/foo/bar", NewPRMMatchRepoDigestOrExact()) - }, - otherJSONParser: func(validJSON []byte) (interface{}, error) { - return newPolicyRequirementFromJSON(validJSON) - }, - duplicateFields: []string{"type", "keyPath", "signedIdentity"}, - }.run(t) - - var pr prSigstoreSigned - - // Start with a valid JSON. - _, validJSON := keyDataTests.validObjectAndJSON(t) - - // Various allowed modifications to the requirement - allowedModificationFns := []func(mSI){ - // Delete the signedIdentity field - func(v mSI) { delete(v, "signedIdentity") }, - } - for _, fn := range allowedModificationFns { - err := tryUnmarshalModifiedSigstoreSigned(t, &pr, validJSON, fn) - require.NoError(t, err) - } - - // Various ways to set signedIdentity to the default value - signedIdentityDefaultFns := []func(mSI){ - // Set signedIdentity to the default explicitly - func(v mSI) { v["signedIdentity"] = NewPRMMatchRepoDigestOrExact() }, - // Delete the signedIdentity field - func(v mSI) { delete(v, "signedIdentity") }, - } - for _, fn := range signedIdentityDefaultFns { - err := tryUnmarshalModifiedSigstoreSigned(t, &pr, validJSON, fn) - require.NoError(t, err) - assert.Equal(t, NewPRMMatchRepoDigestOrExact(), pr.SignedIdentity) - } -} - func TestNewPolicyReferenceMatchFromJSON(t *testing.T) { // Sample success. Others tested in the individual PolicyReferenceMatch.UnmarshalJSON implementations. validPRM := NewPRMMatchRepoDigestOrExact() @@ -1189,7 +1020,7 @@ func TestNewPolicyReferenceMatchFromJSON(t *testing.T) { assert.Equal(t, validPRM, prm) // Invalid - for _, invalid := range []interface{}{ + for _, invalid := range []any{ // Not an object 1, // Missing type @@ -1218,15 +1049,13 @@ func TestNewPRMMatchExact(t *testing.T) { } func TestPRMMatchExactUnmarshalJSON(t *testing.T) { - policyJSONUmarshallerTests{ + policyJSONUmarshallerTests[PolicyReferenceMatch]{ newDest: func() json.Unmarshaler { return &prmMatchExact{} }, - newValidObject: func() (interface{}, error) { + newValidObject: func() (PolicyReferenceMatch, error) { return NewPRMMatchExact(), nil }, - otherJSONParser: func(validJSON []byte) (interface{}, error) { - return newPolicyReferenceMatchFromJSON(validJSON) - }, - invalidObjects: []mSI{ + otherJSONParser: newPolicyReferenceMatchFromJSON, + invalidObjects: []mSA{ // Missing "type" field {}, // Wrong "type" field @@ -1250,15 +1079,13 @@ func TestNewPRMMatchRepoDigestOrExact(t *testing.T) { } func TestPRMMatchRepoDigestOrExactUnmarshalJSON(t *testing.T) { - policyJSONUmarshallerTests{ + policyJSONUmarshallerTests[PolicyReferenceMatch]{ newDest: func() json.Unmarshaler { return &prmMatchRepoDigestOrExact{} }, - newValidObject: func() (interface{}, error) { + newValidObject: func() (PolicyReferenceMatch, error) { return NewPRMMatchRepoDigestOrExact(), nil }, - otherJSONParser: func(validJSON []byte) (interface{}, error) { - return newPolicyReferenceMatchFromJSON(validJSON) - }, - invalidObjects: []mSI{ + otherJSONParser: newPolicyReferenceMatchFromJSON, + invalidObjects: []mSA{ // Missing "type" field {}, // Wrong "type" field @@ -1282,15 +1109,13 @@ func TestNewPRMMatchRepository(t *testing.T) { } func TestPRMMatchRepositoryUnmarshalJSON(t *testing.T) { - policyJSONUmarshallerTests{ + policyJSONUmarshallerTests[PolicyReferenceMatch]{ newDest: func() json.Unmarshaler { return &prmMatchRepository{} }, - newValidObject: func() (interface{}, error) { + newValidObject: func() (PolicyReferenceMatch, error) { return NewPRMMatchRepository(), nil }, - otherJSONParser: func(validJSON []byte) (interface{}, error) { - return newPolicyReferenceMatchFromJSON(validJSON) - }, - invalidObjects: []mSI{ + otherJSONParser: newPolicyReferenceMatchFromJSON, + invalidObjects: []mSA{ // Missing "type" field {}, // Wrong "type" field @@ -1340,26 +1165,24 @@ func TestNewPRMExactReference(t *testing.T) { } func TestPRMExactReferenceUnmarshalJSON(t *testing.T) { - policyJSONUmarshallerTests{ + policyJSONUmarshallerTests[PolicyReferenceMatch]{ newDest: func() json.Unmarshaler { return &prmExactReference{} }, - newValidObject: func() (interface{}, error) { + newValidObject: func() (PolicyReferenceMatch, error) { return NewPRMExactReference("library/busybox:latest") }, - otherJSONParser: func(validJSON []byte) (interface{}, error) { - return newPolicyReferenceMatchFromJSON(validJSON) - }, - breakFns: []func(mSI){ + otherJSONParser: newPolicyReferenceMatchFromJSON, + breakFns: []func(mSA){ // The "type" field is missing - func(v mSI) { delete(v, "type") }, + func(v mSA) { delete(v, "type") }, // Wrong "type" field - func(v mSI) { v["type"] = 1 }, - func(v mSI) { v["type"] = "this is invalid" }, + func(v mSA) { v["type"] = 1 }, + func(v mSA) { v["type"] = "this is invalid" }, // Extra top-level sub-object - func(v mSI) { v["unexpected"] = 1 }, + func(v mSA) { v["unexpected"] = 1 }, // The "dockerReference" field is missing - func(v mSI) { delete(v, "dockerReference") }, + func(v mSA) { delete(v, "dockerReference") }, // Invalid "dockerReference" field - func(v mSI) { v["dockerReference"] = 1 }, + func(v mSA) { v["dockerReference"] = 1 }, }, duplicateFields: []string{"type", "dockerReference"}, }.run(t) @@ -1396,26 +1219,24 @@ func TestNewPRMExactRepository(t *testing.T) { } func TestPRMExactRepositoryUnmarshalJSON(t *testing.T) { - policyJSONUmarshallerTests{ + policyJSONUmarshallerTests[PolicyReferenceMatch]{ newDest: func() json.Unmarshaler { return &prmExactRepository{} }, - newValidObject: func() (interface{}, error) { + newValidObject: func() (PolicyReferenceMatch, error) { return NewPRMExactRepository("library/busybox:latest") }, - otherJSONParser: func(validJSON []byte) (interface{}, error) { - return newPolicyReferenceMatchFromJSON(validJSON) - }, - breakFns: []func(mSI){ + otherJSONParser: newPolicyReferenceMatchFromJSON, + breakFns: []func(mSA){ // The "type" field is missing - func(v mSI) { delete(v, "type") }, + func(v mSA) { delete(v, "type") }, // Wrong "type" field - func(v mSI) { v["type"] = 1 }, - func(v mSI) { v["type"] = "this is invalid" }, + func(v mSA) { v["type"] = 1 }, + func(v mSA) { v["type"] = "this is invalid" }, // Extra top-level sub-object - func(v mSI) { v["unexpected"] = 1 }, + func(v mSA) { v["unexpected"] = 1 }, // The "dockerRepository" field is missing - func(v mSI) { delete(v, "dockerRepository") }, + func(v mSA) { delete(v, "dockerRepository") }, // Invalid "dockerRepository" field - func(v mSI) { v["dockerRepository"] = 1 }, + func(v mSA) { v["dockerRepository"] = 1 }, }, duplicateFields: []string{"type", "dockerRepository"}, }.run(t) @@ -1490,32 +1311,30 @@ func TestNewPRMRemapIdentity(t *testing.T) { } func TestPRMRemapIdentityUnmarshalJSON(t *testing.T) { - policyJSONUmarshallerTests{ + policyJSONUmarshallerTests[PolicyReferenceMatch]{ newDest: func() json.Unmarshaler { return &prmRemapIdentity{} }, - newValidObject: func() (interface{}, error) { + newValidObject: func() (PolicyReferenceMatch, error) { return NewPRMRemapIdentity("example.com/docker-library", "docker.io/library") }, - otherJSONParser: func(validJSON []byte) (interface{}, error) { - return newPolicyReferenceMatchFromJSON(validJSON) - }, - breakFns: []func(mSI){ + otherJSONParser: newPolicyReferenceMatchFromJSON, + breakFns: []func(mSA){ // The "type" field is missing - func(v mSI) { delete(v, "type") }, + func(v mSA) { delete(v, "type") }, // Wrong "type" field - func(v mSI) { v["type"] = 1 }, - func(v mSI) { v["type"] = "this is invalid" }, + func(v mSA) { v["type"] = 1 }, + func(v mSA) { v["type"] = "this is invalid" }, // Extra top-level sub-object - func(v mSI) { v["unexpected"] = 1 }, + func(v mSA) { v["unexpected"] = 1 }, // The "prefix" field is missing - func(v mSI) { delete(v, "prefix") }, + func(v mSA) { delete(v, "prefix") }, // Invalid "prefix" field - func(v mSI) { v["prefix"] = 1 }, - func(v mSI) { v["prefix"] = "this is invalid" }, + func(v mSA) { v["prefix"] = 1 }, + func(v mSA) { v["prefix"] = "this is invalid" }, // The "signedPrefix" field is missing - func(v mSI) { delete(v, "signedPrefix") }, + func(v mSA) { delete(v, "signedPrefix") }, // Invalid "signedPrefix" field - func(v mSI) { v["signedPrefix"] = 1 }, - func(v mSI) { v["signedPrefix"] = "this is invalid" }, + func(v mSA) { v["signedPrefix"] = 1 }, + func(v mSA) { v["signedPrefix"] = "this is invalid" }, }, duplicateFields: []string{"type", "prefix", "signedPrefix"}, }.run(t) diff --git a/signature/policy_eval.go b/signature/policy_eval.go index 533a997b1c..4f8d0da389 100644 --- a/signature/policy_eval.go +++ b/signature/policy_eval.go @@ -46,7 +46,7 @@ type PolicyRequirement interface { // - sarRejected if the signature has not been verified; // in that case error must be non-nil, and should be an PolicyRequirementError if evaluation // succeeded but the result was rejection. - // - sarUnknown if if this PolicyRequirement does not deal with signatures. + // - sarUnknown if this PolicyRequirement does not deal with signatures. // NOTE: sarUnknown should not be returned if this PolicyRequirement should make a decision but something failed. // Returning sarUnknown and a non-nil error value is invalid. // WARNING: This makes the signature contents acceptable for further processing, diff --git a/signature/policy_eval_signedby.go b/signature/policy_eval_signedby.go index ef98b8b83f..a4187735b7 100644 --- a/signature/policy_eval_signedby.go +++ b/signature/policy_eval_signedby.go @@ -12,6 +12,7 @@ import ( "github.com/containers/image/v5/internal/private" "github.com/containers/image/v5/manifest" digest "github.com/opencontainers/go-digest" + "golang.org/x/exp/slices" ) func (pr *prSignedBy) isSignatureAuthorAccepted(ctx context.Context, image private.UnparsedImage, sig []byte) (signatureAcceptanceResult, *Signature, error) { @@ -67,10 +68,8 @@ func (pr *prSignedBy) isSignatureAuthorAccepted(ctx context.Context, image priva signature, err := verifyAndExtractSignature(mech, sig, signatureAcceptanceRules{ validateKeyIdentity: func(keyIdentity string) error { - for _, trustedIdentity := range trustedIdentities { - if keyIdentity == trustedIdentity { - return nil - } + if slices.Contains(trustedIdentities, keyIdentity) { + return nil } // Coverage: We use a private GPG home directory and only import trusted keys, so this should // not be reachable. diff --git a/signature/policy_eval_sigstore.go b/signature/policy_eval_sigstore.go index ccf1d80ac8..dcf5592a8e 100644 --- a/signature/policy_eval_sigstore.go +++ b/signature/policy_eval_sigstore.go @@ -4,6 +4,9 @@ package signature import ( "context" + "crypto" + "crypto/ecdsa" + "crypto/x509" "errors" "fmt" "os" @@ -17,6 +20,100 @@ import ( "github.com/sigstore/sigstore/pkg/cryptoutils" ) +// loadBytesFromDataOrPath ensures there is at most one of ${prefix}Data and ${prefix}Path set, +// and returns the referenced data, or nil if neither is set. +func loadBytesFromDataOrPath(prefix string, data []byte, path string) ([]byte, error) { + switch { + case data != nil && path != "": + return nil, fmt.Errorf(`Internal inconsistency: both "%sPath" and "%sData" specified`, prefix, prefix) + case path != "": + d, err := os.ReadFile(path) + if err != nil { + return nil, err + } + return d, nil + case data != nil: + return data, nil + default: // Nothing + return nil, nil + } +} + +// prepareTrustRoot creates a fulcioTrustRoot from the input data. +// (This also prevents external implementations of this interface, ensuring that prSigstoreSignedFulcio is the only one.) +func (f *prSigstoreSignedFulcio) prepareTrustRoot() (*fulcioTrustRoot, error) { + caCertBytes, err := loadBytesFromDataOrPath("fulcioCA", f.CAData, f.CAPath) + if err != nil { + return nil, err + } + if caCertBytes == nil { + return nil, errors.New(`Internal inconsistency: Fulcio specified with neither "caPath" nor "caData"`) + } + certs := x509.NewCertPool() + if ok := certs.AppendCertsFromPEM(caCertBytes); !ok { + return nil, errors.New("error loading Fulcio CA certificates") + } + fulcio := fulcioTrustRoot{ + caCertificates: certs, + oidcIssuer: f.OIDCIssuer, + subjectEmail: f.SubjectEmail, + } + if err := fulcio.validate(); err != nil { + return nil, err + } + return &fulcio, nil +} + +// sigstoreSignedTrustRoot contains an already parsed version of the prSigstoreSigned policy +type sigstoreSignedTrustRoot struct { + publicKey crypto.PublicKey + fulcio *fulcioTrustRoot + rekorPublicKey *ecdsa.PublicKey +} + +func (pr *prSigstoreSigned) prepareTrustRoot() (*sigstoreSignedTrustRoot, error) { + res := sigstoreSignedTrustRoot{} + + publicKeyPEM, err := loadBytesFromDataOrPath("key", pr.KeyData, pr.KeyPath) + if err != nil { + return nil, err + } + if publicKeyPEM != nil { + pk, err := cryptoutils.UnmarshalPEMToPublicKey(publicKeyPEM) + if err != nil { + return nil, fmt.Errorf("parsing public key: %w", err) + } + res.publicKey = pk + } + + if pr.Fulcio != nil { + f, err := pr.Fulcio.prepareTrustRoot() + if err != nil { + return nil, err + } + res.fulcio = f + } + + rekorPublicKeyPEM, err := loadBytesFromDataOrPath("rekorPublicKey", pr.RekorPublicKeyData, pr.RekorPublicKeyPath) + if err != nil { + return nil, err + } + if rekorPublicKeyPEM != nil { + pk, err := cryptoutils.UnmarshalPEMToPublicKey(rekorPublicKeyPEM) + if err != nil { + return nil, fmt.Errorf("parsing Rekor public key: %w", err) + } + pkECDSA, ok := pk.(*ecdsa.PublicKey) + if !ok { + return nil, fmt.Errorf("Rekor public key is not using ECDSA") + + } + res.rekorPublicKey = pkECDSA + } + + return &res, nil +} + func (pr *prSigstoreSigned) isSignatureAuthorAccepted(ctx context.Context, image private.UnparsedImage, sig []byte) (signatureAcceptanceResult, *Signature, error) { // We don’t know of a single user of this API, and we might return unexpected values in Signature. // For now, just punt. @@ -24,24 +121,10 @@ func (pr *prSigstoreSigned) isSignatureAuthorAccepted(ctx context.Context, image } func (pr *prSigstoreSigned) isSignatureAccepted(ctx context.Context, image private.UnparsedImage, sig signature.Sigstore) (signatureAcceptanceResult, error) { - if pr.KeyPath != "" && pr.KeyData != nil { - return sarRejected, errors.New(`Internal inconsistency: both "keyPath" and "keyData" specified`) - } // FIXME: move this to per-context initialization - var publicKeyPEM []byte - if pr.KeyData != nil { - publicKeyPEM = pr.KeyData - } else { - d, err := os.ReadFile(pr.KeyPath) - if err != nil { - return sarRejected, err - } - publicKeyPEM = d - } - - publicKey, err := cryptoutils.UnmarshalPEMToPublicKey(publicKeyPEM) + trustRoot, err := pr.prepareTrustRoot() if err != nil { - return sarRejected, fmt.Errorf("parsing public key: %w", err) + return sarRejected, err } untrustedAnnotations := sig.UntrustedAnnotations() @@ -49,8 +132,66 @@ func (pr *prSigstoreSigned) isSignatureAccepted(ctx context.Context, image priva if !ok { return sarRejected, fmt.Errorf("missing %s annotation", signature.SigstoreSignatureAnnotationKey) } + untrustedPayload := sig.UntrustedPayload() + + var publicKey crypto.PublicKey + switch { + case trustRoot.publicKey != nil && trustRoot.fulcio != nil: // newPRSigstoreSigned rejects such combinations. + return sarRejected, errors.New("Internal inconsistency: Both a public key and Fulcio CA specified") + case trustRoot.publicKey == nil && trustRoot.fulcio == nil: // newPRSigstoreSigned rejects such combinations. + return sarRejected, errors.New("Internal inconsistency: Neither a public key nor a Fulcio CA specified") - signature, err := internal.VerifySigstorePayload(publicKey, sig.UntrustedPayload(), untrustedBase64Signature, internal.SigstorePayloadAcceptanceRules{ + case trustRoot.publicKey != nil: + if trustRoot.rekorPublicKey != nil { + untrustedSET, ok := untrustedAnnotations[signature.SigstoreSETAnnotationKey] + if !ok { // For user convenience; passing an empty []byte to VerifyRekorSet should work. + return sarRejected, fmt.Errorf("missing %s annotation", signature.SigstoreSETAnnotationKey) + } + // We could use publicKeyPEM directly, but let’s re-marshal to avoid inconsistencies. + // FIXME: We could just generate DER instead of the full PEM text + recreatedPublicKeyPEM, err := cryptoutils.MarshalPublicKeyToPEM(trustRoot.publicKey) + if err != nil { + // Coverage: The key was loaded from a PEM format, so it’s unclear how this could fail. + // (PEM is not essential, MarshalPublicKeyToPEM can only fail if marshaling to ASN1.DER fails.) + return sarRejected, fmt.Errorf("re-marshaling public key to PEM: %w", err) + + } + // We don’t care about the Rekor timestamp, just about log presence. + if _, err := internal.VerifyRekorSET(trustRoot.rekorPublicKey, []byte(untrustedSET), recreatedPublicKeyPEM, untrustedBase64Signature, untrustedPayload); err != nil { + return sarRejected, err + } + } + publicKey = trustRoot.publicKey + + case trustRoot.fulcio != nil: + if trustRoot.rekorPublicKey == nil { // newPRSigstoreSigned rejects such combinations. + return sarRejected, errors.New("Internal inconsistency: Fulcio CA specified without a Rekor public key") + } + untrustedSET, ok := untrustedAnnotations[signature.SigstoreSETAnnotationKey] + if !ok { // For user convenience; passing an empty []byte to VerifyRekorSet should correctly reject it anyway. + return sarRejected, fmt.Errorf("missing %s annotation", signature.SigstoreSETAnnotationKey) + } + untrustedCert, ok := untrustedAnnotations[signature.SigstoreCertificateAnnotationKey] + if !ok { // For user convenience; passing an empty []byte to VerifyRekorSet should correctly reject it anyway. + return sarRejected, fmt.Errorf("missing %s annotation", signature.SigstoreCertificateAnnotationKey) + } + var untrustedIntermediateChainBytes []byte + if untrustedIntermediateChain, ok := untrustedAnnotations[signature.SigstoreIntermediateCertificateChainAnnotationKey]; ok { + untrustedIntermediateChainBytes = []byte(untrustedIntermediateChain) + } + pk, err := verifyRekorFulcio(trustRoot.rekorPublicKey, trustRoot.fulcio, + []byte(untrustedSET), []byte(untrustedCert), untrustedIntermediateChainBytes, untrustedBase64Signature, untrustedPayload) + if err != nil { + return sarRejected, err + } + publicKey = pk + } + + if publicKey == nil { + // Coverage: This should never happen, we have already excluded the possibility in the switch above. + return sarRejected, fmt.Errorf("Internal inconsistency: publicKey not set before verifying sigstore payload") + } + signature, err := internal.VerifySigstorePayload(publicKey, untrustedPayload, untrustedBase64Signature, internal.SigstorePayloadAcceptanceRules{ ValidateSignedDockerReference: func(ref string) error { if !pr.SignedIdentity.matchesDockerReference(image, ref) { return PolicyRequirementError(fmt.Sprintf("Signature for identity %s is not accepted", ref)) diff --git a/signature/policy_eval_sigstore_test.go b/signature/policy_eval_sigstore_test.go index 499081556b..f4dd11368e 100644 --- a/signature/policy_eval_sigstore_test.go +++ b/signature/policy_eval_sigstore_test.go @@ -9,11 +9,202 @@ import ( "testing" "github.com/containers/image/v5/internal/signature" + "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func TestPRrSigstoreSignedIsSignatureAuthorAccepted(t *testing.T) { +func TestPRSigstoreSignedFulcioPrepareTrustRoot(t *testing.T) { + const testCAPath = "fixtures/fulcio_v1.crt.pem" + testCAData, err := os.ReadFile(testCAPath) + require.NoError(t, err) + const testOIDCIssuer = "https://example.com" + testSubjectEmail := "test@example.com" + + // Success + for _, c := range [][]PRSigstoreSignedFulcioOption{ + { + PRSigstoreSignedFulcioWithCAPath(testCAPath), + PRSigstoreSignedFulcioWithOIDCIssuer(testOIDCIssuer), + PRSigstoreSignedFulcioWithSubjectEmail(testSubjectEmail), + }, + { + PRSigstoreSignedFulcioWithCAData(testCAData), + PRSigstoreSignedFulcioWithOIDCIssuer(testOIDCIssuer), + PRSigstoreSignedFulcioWithSubjectEmail(testSubjectEmail), + }, + } { + f, err := newPRSigstoreSignedFulcio(c...) + require.NoError(t, err) + res, err := f.prepareTrustRoot() + require.NoError(t, err) + assert.NotNil(t, res.caCertificates) // Doing a better test seems hard; we would need to compare .Subjects with a DER encoding. + assert.Equal(t, testOIDCIssuer, res.oidcIssuer) + assert.Equal(t, testSubjectEmail, res.subjectEmail) + } + + // Failure + for _, f := range []prSigstoreSignedFulcio{ // Use a prSigstoreSignedFulcio because these configurations should be rejected by NewPRSigstoreSignedFulcio. + { // Neither CAPath nor CAData specified + OIDCIssuer: testOIDCIssuer, + SubjectEmail: testSubjectEmail, + }, + { // Both CAPath and CAData specified + CAPath: testCAPath, + CAData: testCAData, + OIDCIssuer: testOIDCIssuer, + SubjectEmail: testSubjectEmail, + }, + { // Invalid CAPath + CAPath: "fixtures/image.signature", + OIDCIssuer: testOIDCIssuer, + SubjectEmail: testSubjectEmail, + }, + { // Unusable CAPath + CAPath: "fixtures/this/does/not/exist", + OIDCIssuer: testOIDCIssuer, + SubjectEmail: testSubjectEmail, + }, + { // Invalid CAData + CAData: []byte("invalid"), + OIDCIssuer: testOIDCIssuer, + SubjectEmail: testSubjectEmail, + }, + { // Missing OIDCIssuer + CAPath: testCAPath, + SubjectEmail: testSubjectEmail, + }, + { // Missing SubjectEmail + CAPath: testCAPath, + OIDCIssuer: testOIDCIssuer, + }, + } { + _, err := f.prepareTrustRoot() + assert.Error(t, err) + } +} + +func TestPRSigstoreSignedPrepareTrustRoot(t *testing.T) { + const testKeyPath = "fixtures/cosign.pub" + testKeyData, err := os.ReadFile(testKeyPath) + require.NoError(t, err) + testFulcio, err := NewPRSigstoreSignedFulcio( + PRSigstoreSignedFulcioWithCAPath("fixtures/fulcio_v1.crt.pem"), + PRSigstoreSignedFulcioWithOIDCIssuer("https://github.com/login/oauth"), + PRSigstoreSignedFulcioWithSubjectEmail("mitr@redhat.com"), + ) + require.NoError(t, err) + const testRekorPublicKeyPath = "fixtures/rekor.pub" + testRekorPublicKeyData, err := os.ReadFile(testRekorPublicKeyPath) + require.NoError(t, err) + testIdentity := newPRMMatchRepoDigestOrExact() + testIdentityOption := PRSigstoreSignedWithSignedIdentity(testIdentity) + + // Success with public key + for _, c := range [][]PRSigstoreSignedOption{ + { + PRSigstoreSignedWithKeyPath(testKeyPath), + testIdentityOption, + }, + { + PRSigstoreSignedWithKeyData(testKeyData), + testIdentityOption, + }, + } { + pr, err := newPRSigstoreSigned(c...) + require.NoError(t, err) + res, err := pr.prepareTrustRoot() + require.NoError(t, err) + assert.NotNil(t, res.publicKey) + assert.Nil(t, res.fulcio) + assert.Nil(t, res.rekorPublicKey) + } + // Success with Fulcio + pr, err := newPRSigstoreSigned( + PRSigstoreSignedWithFulcio(testFulcio), + PRSigstoreSignedWithRekorPublicKeyData(testRekorPublicKeyData), + testIdentityOption, + ) + require.NoError(t, err) + res, err := pr.prepareTrustRoot() + require.NoError(t, err) + assert.Nil(t, res.publicKey) + assert.NotNil(t, res.fulcio) + assert.NotNil(t, res.rekorPublicKey) + // Success with Rekor public key + for _, c := range [][]PRSigstoreSignedOption{ + { + PRSigstoreSignedWithKeyData(testKeyData), + PRSigstoreSignedWithRekorPublicKeyPath(testRekorPublicKeyPath), + testIdentityOption, + }, + { + PRSigstoreSignedWithKeyData(testKeyData), + PRSigstoreSignedWithRekorPublicKeyData(testRekorPublicKeyData), + testIdentityOption, + }, + } { + pr, err := newPRSigstoreSigned(c...) + require.NoError(t, err) + res, err := pr.prepareTrustRoot() + require.NoError(t, err) + assert.NotNil(t, res.publicKey) + assert.Nil(t, res.fulcio) + assert.NotNil(t, res.rekorPublicKey) + } + + // Failure + for _, pr := range []prSigstoreSigned{ // Use a prSigstoreSigned because these configurations should be rejected by NewPRSigstoreSigned. + { // Both KeyPath and KeyData specified + KeyPath: testKeyPath, + KeyData: testKeyData, + SignedIdentity: testIdentity, + }, + { // Invalid public key path + KeyPath: "fixtures/image.signature", + SignedIdentity: testIdentity, + }, + { // Unusable public key path + KeyPath: "fixtures/this/does/not/exist", + SignedIdentity: testIdentity, + }, + { // Invalid public key data + KeyData: []byte("this is invalid"), + SignedIdentity: testIdentity, + }, + { // Invalid Fulcio configuration + Fulcio: &prSigstoreSignedFulcio{}, + RekorPublicKeyData: testKeyData, + SignedIdentity: testIdentity, + }, + { // Both RekorPublicKeyPath and RekorPublicKeyData specified + KeyData: testKeyData, + RekorPublicKeyPath: testRekorPublicKeyPath, + RekorPublicKeyData: testRekorPublicKeyData, + SignedIdentity: testIdentity, + }, + { // Invalid Rekor public key path + KeyData: testKeyData, + RekorPublicKeyPath: "fixtures/image.signature", + SignedIdentity: testIdentity, + }, + { // Invalid Rekor public key data + KeyData: testKeyData, + RekorPublicKeyData: []byte("this is invalid"), + SignedIdentity: testIdentity, + }, + { // Rekor public key is not ECDSA + KeyData: testKeyData, + RekorPublicKeyPath: "fixtures/some-rsa-key.pub", + SignedIdentity: testIdentity, + }, + } { + _, err = pr.prepareTrustRoot() + assert.Error(t, err) + } +} + +func TestPRSigstoreSignedIsSignatureAuthorAccepted(t *testing.T) { // Currently, this fails even with a correctly signed image. prm := NewPRMMatchRepository() // We prefer to test with a Cosign-created signature for interoperability, and that doesn’t work with matchExact. testImage := dirImageMock(t, "fixtures/dir-img-cosign-valid", "192.168.64.2:5000/cosign-signed-single-sample") @@ -21,7 +212,10 @@ func TestPRrSigstoreSignedIsSignatureAuthorAccepted(t *testing.T) { require.NoError(t, err) // Successful validation, with KeyData and KeyPath - pr, err := newPRSigstoreSignedKeyPath("fixtures/cosign.pub", prm) + pr, err := newPRSigstoreSigned( + PRSigstoreSignedWithKeyPath("fixtures/cosign.pub"), + PRSigstoreSignedWithSignedIdentity(prm), + ) require.NoError(t, err) sar, parsedSig, err := pr.isSignatureAuthorAccepted(context.Background(), testImage, testImageSigBlob) assertSARRejected(t, sar, parsedSig, err) @@ -38,77 +232,255 @@ func sigstoreSignatureFromFile(t *testing.T, path string) signature.Sigstore { return sig } +// sigstoreSignatureWithoutAnnotation returns a signature.Sigstore based on template +// that is missing the specified annotation. +func sigstoreSignatureWithoutAnnotation(t *testing.T, template signature.Sigstore, annotation string) signature.Sigstore { + annotations := template.UntrustedAnnotations() // This returns a copy that is safe to modify. + require.Contains(t, annotations, annotation) + delete(annotations, annotation) + return signature.SigstoreFromComponents(template.UntrustedMIMEType(), template.UntrustedPayload(), annotations) +} + +// sigstoreSignatureWithModifiedAnnotation returns a signature.Sigstore based on template +// where the specified annotation is replaced +func sigstoreSignatureWithModifiedAnnotation(template signature.Sigstore, annotation, value string) signature.Sigstore { + annotations := template.UntrustedAnnotations() // This returns a copy that is safe to modify. + annotations[annotation] = value + return signature.SigstoreFromComponents(template.UntrustedMIMEType(), template.UntrustedPayload(), annotations) +} + func TestPRrSigstoreSignedIsSignatureAccepted(t *testing.T) { assertAccepted := func(sar signatureAcceptanceResult, err error) { assert.Equal(t, sarAccepted, sar) assert.NoError(t, err) } assertRejected := func(sar signatureAcceptanceResult, err error) { + logrus.Errorf("%v", err) assert.Equal(t, sarRejected, sar) assert.Error(t, err) } prm := NewPRMMatchRepository() // We prefer to test with a Cosign-created signature to ensure interoperability, and that doesn’t work with matchExact. matchExact is tested later. - testImage := dirImageMock(t, "fixtures/dir-img-cosign-valid", "192.168.64.2:5000/cosign-signed-single-sample") - testImageSig := sigstoreSignatureFromFile(t, "fixtures/dir-img-cosign-valid/signature-1") - - // Successful validation, with KeyData and KeyPath - pr, err := newPRSigstoreSignedKeyPath("fixtures/cosign.pub", prm) - require.NoError(t, err) - sar, err := pr.isSignatureAccepted(context.Background(), testImage, testImageSig) - assertAccepted(sar, err) - + testKeyImage := dirImageMock(t, "fixtures/dir-img-cosign-valid", "192.168.64.2:5000/cosign-signed-single-sample") + testKeyImageSig := sigstoreSignatureFromFile(t, "fixtures/dir-img-cosign-valid/signature-1") + testKeyRekorImage := dirImageMock(t, "fixtures/dir-img-cosign-key-rekor-valid", "192.168.64.2:5000/cosign-signed/key-1") + testKeyRekorImageSig := sigstoreSignatureFromFile(t, "fixtures/dir-img-cosign-key-rekor-valid/signature-1") + testFulcioRekorImage := dirImageMock(t, "fixtures/dir-img-cosign-fulcio-rekor-valid", "192.168.64.2:5000/cosign-signed/fulcio-rekor-1") + testFulcioRekorImageSig := sigstoreSignatureFromFile(t, "fixtures/dir-img-cosign-fulcio-rekor-valid/signature-1") keyData, err := os.ReadFile("fixtures/cosign.pub") require.NoError(t, err) - pr, err = newPRSigstoreSignedKeyData(keyData, prm) + + // prepareTrustRoot fails + pr := &prSigstoreSigned{ + KeyPath: "fixtures/cosign.pub", + KeyData: keyData, + SignedIdentity: prm, + } + // Pass a nil pointer to, kind of, test that the return value does not depend on the image. + sar, err := pr.isSignatureAccepted(context.Background(), nil, testKeyImageSig) + assertRejected(sar, err) + + // Signature has no cryptographic signature + pr, err = newPRSigstoreSigned( + PRSigstoreSignedWithKeyPath("fixtures/cosign.pub"), + PRSigstoreSignedWithSignedIdentity(prm), + ) require.NoError(t, err) - sar, err = pr.isSignatureAccepted(context.Background(), testImage, testImageSig) - assertAccepted(sar, err) + // Pass a nil pointer to, kind of, test that the return value does not depend on the image. + sar, err = pr.isSignatureAccepted(context.Background(), nil, + signature.SigstoreFromComponents(testKeyImageSig.UntrustedMIMEType(), testKeyImageSig.UntrustedPayload(), nil)) + assertRejected(sar, err) - // Both KeyPath and KeyData set. Do not use newPRSigstoreSigned*, because it would reject this. + // Neither a public key nor Fulcio is specified pr = &prSigstoreSigned{ - KeyPath: "/foo/bar", - KeyData: []byte("abc"), SignedIdentity: prm, } - // Pass nil and empty data to, kind of, test that the return value does not depend on the image. - sar, err = pr.isSignatureAccepted(context.Background(), nil, testImageSig) + // Pass a nil pointer to, kind of, test that the return value does not depend on the image. + sar, err = pr.isSignatureAccepted(context.Background(), nil, testKeyImageSig) assertRejected(sar, err) - // Invalid KeyPath - pr, err = newPRSigstoreSignedKeyPath("/this/does/not/exist", prm) + // Both a public key and Fulcio is specified + fulcio, err := NewPRSigstoreSignedFulcio( + PRSigstoreSignedFulcioWithCAPath("fixtures/fulcio_v1.crt.pem"), + PRSigstoreSignedFulcioWithOIDCIssuer("https://github.com/login/oauth"), + PRSigstoreSignedFulcioWithSubjectEmail("mitr@redhat.com"), + ) require.NoError(t, err) - // Pass nil and empty data to, kind of, test that the return value does not depend on the image. - sar, err = pr.isSignatureAccepted(context.Background(), nil, testImageSig) + pr = &prSigstoreSigned{ + KeyPath: "fixtures/cosign.pub", + Fulcio: fulcio, + SignedIdentity: prm, + } + // Pass a nil pointer to, kind of, test that the return value does not depend on the image. + sar, err = pr.isSignatureAccepted(context.Background(), nil, testKeyImageSig) assertRejected(sar, err) - // KeyData doesn’t contain a public key. - pr, err = newPRSigstoreSignedKeyData([]byte{}, prm) + // Successful key+Rekor use + pr, err = newPRSigstoreSigned( + PRSigstoreSignedWithKeyPath("fixtures/cosign2.pub"), + PRSigstoreSignedWithRekorPublicKeyPath("fixtures/rekor.pub"), + PRSigstoreSignedWithSignedIdentity(prm), + ) + require.NoError(t, err) + sar, err = pr.isSignatureAccepted(context.Background(), testKeyRekorImage, testKeyRekorImageSig) + assertAccepted(sar, err) + + // key+Rekor, missing Rekor SET annotation + sar, err = pr.isSignatureAccepted(context.Background(), nil, + sigstoreSignatureWithoutAnnotation(t, testKeyRekorImageSig, signature.SigstoreSETAnnotationKey)) + assertRejected(sar, err) + // Actual Rekor logic is unit-tested elsewhere, but smoke-test the basics: + // key+Rekor: Invalid Rekor SET + // Pass a nil pointer to, kind of, test that the return value does not depend on the image. + sar, err = pr.isSignatureAccepted(context.Background(), nil, + sigstoreSignatureWithModifiedAnnotation(testKeyRekorImageSig, signature.SigstoreSETAnnotationKey, + "this is not a valid SET")) + assertRejected(sar, err) + // Fulcio: A Rekor SET which we don’t accept (one of many reasons) + pr2, err := newPRSigstoreSigned( + PRSigstoreSignedWithKeyPath("fixtures/cosign2.pub"), + PRSigstoreSignedWithRekorPublicKeyPath("fixtures/cosign.pub"), // not rekor.pub = a key mismatch + PRSigstoreSignedWithSignedIdentity(prm), + ) require.NoError(t, err) - // Pass nil and empty data to, kind of, test that the return value does not depend on the image. - sar, err = pr.isSignatureAccepted(context.Background(), nil, testImageSig) + // Pass a nil pointer to, kind of, test that the return value does not depend on the image. + sar, err = pr2.isSignatureAccepted(context.Background(), nil, testKeyRekorImageSig) assertRejected(sar, err) - // Signature has no cryptographic signature - pr, err = newPRSigstoreSignedKeyPath("fixtures/cosign.pub", prm) + // Successful Fulcio certificate use + fulcio, err = NewPRSigstoreSignedFulcio( + PRSigstoreSignedFulcioWithCAPath("fixtures/fulcio_v1.crt.pem"), + PRSigstoreSignedFulcioWithOIDCIssuer("https://github.com/login/oauth"), + PRSigstoreSignedFulcioWithSubjectEmail("mitr@redhat.com"), + ) + require.NoError(t, err) + pr, err = newPRSigstoreSigned( + PRSigstoreSignedWithFulcio(fulcio), + PRSigstoreSignedWithRekorPublicKeyPath("fixtures/rekor.pub"), + PRSigstoreSignedWithSignedIdentity(prm), + ) + require.NoError(t, err) + sar, err = pr.isSignatureAccepted(context.Background(), testFulcioRekorImage, + testFulcioRekorImageSig) + assertAccepted(sar, err) + + // Fulcio, no Rekor requirement + pr2 = &prSigstoreSigned{ + Fulcio: fulcio, + SignedIdentity: prm, + } + // Pass a nil pointer to, kind of, test that the return value does not depend on the image. + sar, err = pr2.isSignatureAccepted(context.Background(), nil, + sigstoreSignatureWithoutAnnotation(t, testFulcioRekorImageSig, signature.SigstoreSETAnnotationKey)) + assertRejected(sar, err) + // Fulcio, missing Rekor SET annotation + // Pass a nil pointer to, kind of, test that the return value does not depend on the image. + sar, err = pr.isSignatureAccepted(context.Background(), nil, + sigstoreSignatureWithoutAnnotation(t, testFulcioRekorImageSig, signature.SigstoreSETAnnotationKey)) + assertRejected(sar, err) + // Fulcio, missing certificate annotation + // Pass a nil pointer to, kind of, test that the return value does not depend on the image. + sar, err = pr.isSignatureAccepted(context.Background(), nil, + sigstoreSignatureWithoutAnnotation(t, testFulcioRekorImageSig, signature.SigstoreCertificateAnnotationKey)) + assertRejected(sar, err) + // Fulcio: missing certificate chain annotation causes the Cosign-issued signature to be rejected + // because there is no path to the trusted CA + // Pass a nil pointer to, kind of, test that the return value does not depend on the image. + sar, err = pr.isSignatureAccepted(context.Background(), nil, + sigstoreSignatureWithoutAnnotation(t, testFulcioRekorImageSig, signature.SigstoreIntermediateCertificateChainAnnotationKey)) + assertRejected(sar, err) + // … but a signature without the intermediate annotation is fine if the issuer is directly trusted + // (which we handle by trusing the intermediates) + fulcio2, err := NewPRSigstoreSignedFulcio( + PRSigstoreSignedFulcioWithCAData([]byte(testFulcioRekorImageSig.UntrustedAnnotations()[signature.SigstoreIntermediateCertificateChainAnnotationKey])), + PRSigstoreSignedFulcioWithOIDCIssuer("https://github.com/login/oauth"), + PRSigstoreSignedFulcioWithSubjectEmail("mitr@redhat.com"), + ) + require.NoError(t, err) + pr2, err = newPRSigstoreSigned( + PRSigstoreSignedWithFulcio(fulcio2), + PRSigstoreSignedWithRekorPublicKeyPath("fixtures/rekor.pub"), + PRSigstoreSignedWithSignedIdentity(prm), + ) + require.NoError(t, err) + sar, err = pr2.isSignatureAccepted(context.Background(), testFulcioRekorImage, + sigstoreSignatureWithoutAnnotation(t, testFulcioRekorImageSig, signature.SigstoreIntermediateCertificateChainAnnotationKey)) + assertAccepted(sar, err) + // Actual Fulcio and Rekor logic is unit-tested elsewhere, but smoke-test the basics: + // Fulcio: Invalid Fulcio certificate + // Pass a nil pointer to, kind of, test that the return value does not depend on the image. + sar, err = pr.isSignatureAccepted(context.Background(), nil, + sigstoreSignatureWithModifiedAnnotation(testFulcioRekorImageSig, signature.SigstoreCertificateAnnotationKey, + "this is not a valid certificate")) + assertRejected(sar, err) + // Fulcio: A Fulcio certificate which we don’t accept (one of many reasons) + fulcio2, err = NewPRSigstoreSignedFulcio( + PRSigstoreSignedFulcioWithCAPath("fixtures/fulcio_v1.crt.pem"), + PRSigstoreSignedFulcioWithOIDCIssuer("https://github.com/login/oauth"), + PRSigstoreSignedFulcioWithSubjectEmail("this-does-not-match@example.com"), + ) + require.NoError(t, err) + pr2, err = newPRSigstoreSigned( + PRSigstoreSignedWithFulcio(fulcio2), + PRSigstoreSignedWithRekorPublicKeyPath("fixtures/rekor.pub"), + PRSigstoreSignedWithSignedIdentity(prm), + ) require.NoError(t, err) // Pass a nil pointer to, kind of, test that the return value does not depend on the image. + sar, err = pr2.isSignatureAccepted(context.Background(), nil, testFulcioRekorImageSig) + assertRejected(sar, err) + // Fulcio: Invalid Rekor SET + // Pass a nil pointer to, kind of, test that the return value does not depend on the image. sar, err = pr.isSignatureAccepted(context.Background(), nil, - signature.SigstoreFromComponents(testImageSig.UntrustedMIMEType(), testImageSig.UntrustedPayload(), nil)) + sigstoreSignatureWithModifiedAnnotation(testFulcioRekorImageSig, signature.SigstoreSETAnnotationKey, + "this is not a valid SET")) assertRejected(sar, err) + // Fulcio: A Rekor SET which we don’t accept (one of many reasons) + pr2, err = newPRSigstoreSigned( + PRSigstoreSignedWithFulcio(fulcio), + PRSigstoreSignedWithRekorPublicKeyPath("fixtures/cosign.pub"), // not rekor.pub = a key mismatch + PRSigstoreSignedWithSignedIdentity(prm), + ) + require.NoError(t, err) + // Pass a nil pointer to, kind of, test that the return value does not depend on the image. + sar, err = pr2.isSignatureAccepted(context.Background(), nil, testFulcioRekorImageSig) + assertRejected(sar, err) + + // Successful validation, with KeyData and KeyPath + pr, err = newPRSigstoreSigned( + PRSigstoreSignedWithKeyPath("fixtures/cosign.pub"), + PRSigstoreSignedWithSignedIdentity(prm), + ) + require.NoError(t, err) + sar, err = pr.isSignatureAccepted(context.Background(), testKeyImage, testKeyImageSig) + assertAccepted(sar, err) + + pr, err = newPRSigstoreSigned( + PRSigstoreSignedWithKeyData(keyData), + PRSigstoreSignedWithSignedIdentity(prm), + ) + require.NoError(t, err) + sar, err = pr.isSignatureAccepted(context.Background(), testKeyImage, testKeyImageSig) + assertAccepted(sar, err) // A signature which does not verify - pr, err = newPRSigstoreSignedKeyPath("fixtures/cosign.pub", prm) + pr, err = newPRSigstoreSigned( + PRSigstoreSignedWithKeyPath("fixtures/cosign.pub"), + PRSigstoreSignedWithSignedIdentity(prm), + ) require.NoError(t, err) // Pass a nil pointer to, kind of, test that the return value does not depend on the image. sar, err = pr.isSignatureAccepted(context.Background(), nil, - signature.SigstoreFromComponents(testImageSig.UntrustedMIMEType(), testImageSig.UntrustedPayload(), map[string]string{ + signature.SigstoreFromComponents(testKeyImageSig.UntrustedMIMEType(), testKeyImageSig.UntrustedPayload(), map[string]string{ signature.SigstoreSignatureAnnotationKey: base64.StdEncoding.EncodeToString([]byte("invalid signature")), })) assertRejected(sar, err) // A valid signature using an unknown key. - pr, err = newPRSigstoreSignedKeyPath("fixtures/cosign.pub", prm) + pr, err = newPRSigstoreSigned( + PRSigstoreSignedWithKeyPath("fixtures/cosign.pub"), + PRSigstoreSignedWithSignedIdentity(prm), + ) require.NoError(t, err) // Pass a nil pointer to, kind of, test that the return value does not depend on the image. sar, err = pr.isSignatureAccepted(context.Background(), nil, sigstoreSignatureFromFile(t, "fixtures/unknown-cosign-key.signature")) @@ -117,28 +489,40 @@ func TestPRrSigstoreSignedIsSignatureAccepted(t *testing.T) { // A valid signature with a rejected identity. nonmatchingPRM, err := NewPRMExactReference("this/doesnt:match") require.NoError(t, err) - pr, err = newPRSigstoreSignedKeyPath("fixtures/cosign.pub", nonmatchingPRM) + pr, err = newPRSigstoreSigned( + PRSigstoreSignedWithKeyPath("fixtures/cosign.pub"), + PRSigstoreSignedWithSignedIdentity(nonmatchingPRM), + ) require.NoError(t, err) - sar, err = pr.isSignatureAccepted(context.Background(), testImage, testImageSig) + sar, err = pr.isSignatureAccepted(context.Background(), testKeyImage, testKeyImageSig) assertRejected(sar, err) // Error reading image manifest image := dirImageMock(t, "fixtures/dir-img-cosign-no-manifest", "192.168.64.2:5000/cosign-signed-single-sample") - pr, err = newPRSigstoreSignedKeyPath("fixtures/cosign.pub", prm) + pr, err = newPRSigstoreSigned( + PRSigstoreSignedWithKeyPath("fixtures/cosign.pub"), + PRSigstoreSignedWithSignedIdentity(prm), + ) require.NoError(t, err) sar, err = pr.isSignatureAccepted(context.Background(), image, sigstoreSignatureFromFile(t, "fixtures/dir-img-cosign-no-manifest/signature-1")) assertRejected(sar, err) // Error computing manifest digest image = dirImageMock(t, "fixtures/dir-img-cosign-manifest-digest-error", "192.168.64.2:5000/cosign-signed-single-sample") - pr, err = newPRSigstoreSignedKeyPath("fixtures/cosign.pub", prm) + pr, err = newPRSigstoreSigned( + PRSigstoreSignedWithKeyPath("fixtures/cosign.pub"), + PRSigstoreSignedWithSignedIdentity(prm), + ) require.NoError(t, err) sar, err = pr.isSignatureAccepted(context.Background(), image, sigstoreSignatureFromFile(t, "fixtures/dir-img-cosign-manifest-digest-error/signature-1")) assertRejected(sar, err) // A valid signature with a non-matching manifest image = dirImageMock(t, "fixtures/dir-img-cosign-modified-manifest", "192.168.64.2:5000/cosign-signed-single-sample") - pr, err = newPRSigstoreSignedKeyPath("fixtures/cosign.pub", prm) + pr, err = newPRSigstoreSigned( + PRSigstoreSignedWithKeyPath("fixtures/cosign.pub"), + PRSigstoreSignedWithSignedIdentity(prm), + ) require.NoError(t, err) sar, err = pr.isSignatureAccepted(context.Background(), image, sigstoreSignatureFromFile(t, "fixtures/dir-img-cosign-modified-manifest/signature-1")) assertRejected(sar, err) @@ -146,20 +530,29 @@ func TestPRrSigstoreSignedIsSignatureAccepted(t *testing.T) { // Minimally check that the prmMatchExact also works as expected: // - Signatures with a matching tag work image = dirImageMock(t, "fixtures/dir-img-cosign-valid-with-tag", "192.168.64.2:5000/skopeo-signed:tag") - pr, err = newPRSigstoreSignedKeyPath("fixtures/cosign.pub", NewPRMMatchExact()) + pr, err = newPRSigstoreSigned( + PRSigstoreSignedWithKeyPath("fixtures/cosign.pub"), + PRSigstoreSignedWithSignedIdentity(NewPRMMatchExact()), + ) require.NoError(t, err) sar, err = pr.isSignatureAccepted(context.Background(), image, sigstoreSignatureFromFile(t, "fixtures/dir-img-cosign-valid-with-tag/signature-1")) assertAccepted(sar, err) // - Signatures with a non-matching tag are rejected image = dirImageMock(t, "fixtures/dir-img-cosign-valid-with-tag", "192.168.64.2:5000/skopeo-signed:othertag") - pr, err = newPRSigstoreSignedKeyPath("fixtures/cosign.pub", NewPRMMatchExact()) + pr, err = newPRSigstoreSigned( + PRSigstoreSignedWithKeyPath("fixtures/cosign.pub"), + PRSigstoreSignedWithSignedIdentity(NewPRMMatchExact()), + ) require.NoError(t, err) sar, err = pr.isSignatureAccepted(context.Background(), image, sigstoreSignatureFromFile(t, "fixtures/dir-img-cosign-valid-with-tag/signature-1")) assertRejected(sar, err) // - Cosign-created signatures are rejected - pr, err = newPRSigstoreSignedKeyPath("fixtures/cosign.pub", NewPRMMatchExact()) + pr, err = newPRSigstoreSigned( + PRSigstoreSignedWithKeyPath("fixtures/cosign.pub"), + PRSigstoreSignedWithSignedIdentity(NewPRMMatchExact()), + ) require.NoError(t, err) - sar, err = pr.isSignatureAccepted(context.Background(), testImage, testImageSig) + sar, err = pr.isSignatureAccepted(context.Background(), testKeyImage, testKeyImageSig) assertRejected(sar, err) } @@ -168,7 +561,10 @@ func TestPRSigstoreSignedIsRunningImageAllowed(t *testing.T) { // A simple success case: single valid signature. image := dirImageMock(t, "fixtures/dir-img-cosign-valid", "192.168.64.2:5000/cosign-signed-single-sample") - pr, err := NewPRSigstoreSignedKeyPath("fixtures/cosign.pub", prm) + pr, err := NewPRSigstoreSigned( + PRSigstoreSignedWithKeyPath("fixtures/cosign.pub"), + PRSigstoreSignedWithSignedIdentity(prm), + ) require.NoError(t, err) allowed, err := pr.isRunningImageAllowed(context.Background(), image) assertRunningAllowed(t, allowed, err) @@ -176,56 +572,80 @@ func TestPRSigstoreSignedIsRunningImageAllowed(t *testing.T) { // Error reading signatures invalidSigDir := createInvalidSigDir(t) image = dirImageMock(t, invalidSigDir, "192.168.64.2:5000/cosign-signed-single-sample") - pr, err = NewPRSigstoreSignedKeyPath("fixtures/cosign.pub", prm) + pr, err = NewPRSigstoreSigned( + PRSigstoreSignedWithKeyPath("fixtures/cosign.pub"), + PRSigstoreSignedWithSignedIdentity(prm), + ) require.NoError(t, err) allowed, err = pr.isRunningImageAllowed(context.Background(), image) assertRunningRejected(t, allowed, err) // No signatures image = dirImageMock(t, "fixtures/dir-img-unsigned", "testing/manifest:latest") - pr, err = NewPRSigstoreSignedKeyPath("fixtures/cosign.pub", prm) + pr, err = NewPRSigstoreSigned( + PRSigstoreSignedWithKeyPath("fixtures/cosign.pub"), + PRSigstoreSignedWithSignedIdentity(prm), + ) require.NoError(t, err) allowed, err = pr.isRunningImageAllowed(context.Background(), image) assertRunningRejected(t, allowed, err) // Only non-sigstore signatures image = dirImageMock(t, "fixtures/dir-img-valid", "testing/manifest:latest") - pr, err = NewPRSigstoreSignedKeyPath("fixtures/cosign.pub", prm) + pr, err = NewPRSigstoreSigned( + PRSigstoreSignedWithKeyPath("fixtures/cosign.pub"), + PRSigstoreSignedWithSignedIdentity(prm), + ) require.NoError(t, err) allowed, err = pr.isRunningImageAllowed(context.Background(), image) assertRunningRejected(t, allowed, err) // Only non-signature sigstore attachments image = dirImageMock(t, "fixtures/dir-img-cosign-other-attachment", "testing/manifest:latest") - pr, err = NewPRSigstoreSignedKeyPath("fixtures/cosign.pub", prm) + pr, err = NewPRSigstoreSigned( + PRSigstoreSignedWithKeyPath("fixtures/cosign.pub"), + PRSigstoreSignedWithSignedIdentity(prm), + ) require.NoError(t, err) allowed, err = pr.isRunningImageAllowed(context.Background(), image) assertRunningRejected(t, allowed, err) // 1 invalid signature: use dir-img-valid, but a non-matching Docker reference image = dirImageMock(t, "fixtures/dir-img-cosign-valid", "testing/manifest:notlatest") - pr, err = NewPRSigstoreSignedKeyPath("fixtures/cosign.pub", prm) + pr, err = NewPRSigstoreSigned( + PRSigstoreSignedWithKeyPath("fixtures/cosign.pub"), + PRSigstoreSignedWithSignedIdentity(prm), + ) require.NoError(t, err) allowed, err = pr.isRunningImageAllowed(context.Background(), image) assertRunningRejectedPolicyRequirement(t, allowed, err) // 2 valid signatures image = dirImageMock(t, "fixtures/dir-img-cosign-valid-2", "192.168.64.2:5000/cosign-signed-single-sample") - pr, err = NewPRSigstoreSignedKeyPath("fixtures/cosign.pub", prm) + pr, err = NewPRSigstoreSigned( + PRSigstoreSignedWithKeyPath("fixtures/cosign.pub"), + PRSigstoreSignedWithSignedIdentity(prm), + ) require.NoError(t, err) allowed, err = pr.isRunningImageAllowed(context.Background(), image) assertRunningAllowed(t, allowed, err) // One invalid, one valid signature (in this order) image = dirImageMock(t, "fixtures/dir-img-cosign-mixed", "192.168.64.2:5000/cosign-signed-single-sample") - pr, err = NewPRSigstoreSignedKeyPath("fixtures/cosign.pub", prm) + pr, err = NewPRSigstoreSigned( + PRSigstoreSignedWithKeyPath("fixtures/cosign.pub"), + PRSigstoreSignedWithSignedIdentity(prm), + ) require.NoError(t, err) allowed, err = pr.isRunningImageAllowed(context.Background(), image) assertRunningAllowed(t, allowed, err) // 2 invalid signajtures: use dir-img-cosign-valid-2, but a non-matching Docker reference image = dirImageMock(t, "fixtures/dir-img-cosign-valid-2", "this/doesnt:match") - pr, err = NewPRSigstoreSignedKeyPath("fixtures/cosign.pub", prm) + pr, err = NewPRSigstoreSigned( + PRSigstoreSignedWithKeyPath("fixtures/cosign.pub"), + PRSigstoreSignedWithSignedIdentity(prm), + ) require.NoError(t, err) allowed, err = pr.isRunningImageAllowed(context.Background(), image) assertRunningRejectedPolicyRequirement(t, allowed, err) @@ -233,19 +653,28 @@ func TestPRSigstoreSignedIsRunningImageAllowed(t *testing.T) { // Minimally check that the prmMatchExact also works as expected: // - Signatures with a matching tag work image = dirImageMock(t, "fixtures/dir-img-cosign-valid-with-tag", "192.168.64.2:5000/skopeo-signed:tag") - pr, err = NewPRSigstoreSignedKeyPath("fixtures/cosign.pub", NewPRMMatchExact()) + pr, err = NewPRSigstoreSigned( + PRSigstoreSignedWithKeyPath("fixtures/cosign.pub"), + PRSigstoreSignedWithSignedIdentity(NewPRMMatchExact()), + ) require.NoError(t, err) allowed, err = pr.isRunningImageAllowed(context.Background(), image) assertRunningAllowed(t, allowed, err) // - Signatures with a non-matching tag are rejected image = dirImageMock(t, "fixtures/dir-img-cosign-valid-with-tag", "192.168.64.2:5000/skopeo-signed:othertag") - pr, err = NewPRSigstoreSignedKeyPath("fixtures/cosign.pub", NewPRMMatchExact()) + pr, err = NewPRSigstoreSigned( + PRSigstoreSignedWithKeyPath("fixtures/cosign.pub"), + PRSigstoreSignedWithSignedIdentity(NewPRMMatchExact()), + ) require.NoError(t, err) allowed, err = pr.isRunningImageAllowed(context.Background(), image) assertRunningRejectedPolicyRequirement(t, allowed, err) // - Cosign-created signatures are rejected image = dirImageMock(t, "fixtures/dir-img-cosign-valid", "192.168.64.2:5000/cosign-signed-single-sample") - pr, err = NewPRSigstoreSignedKeyPath("fixtures/cosign.pub", NewPRMMatchExact()) + pr, err = NewPRSigstoreSigned( + PRSigstoreSignedWithKeyPath("fixtures/cosign.pub"), + PRSigstoreSignedWithSignedIdentity(NewPRMMatchExact()), + ) require.NoError(t, err) allowed, err = pr.isRunningImageAllowed(context.Background(), image) assertRunningRejectedPolicyRequirement(t, allowed, err) diff --git a/signature/policy_reference_match_test.go b/signature/policy_reference_match_test.go index f1c99981e3..8707732ee3 100644 --- a/signature/policy_reference_match_test.go +++ b/signature/policy_reference_match_test.go @@ -460,7 +460,7 @@ func modifiedString(t *testing.T, input string) string { c := input[0] switch { case c >= 'a' && c <= 'y': - c = c + 1 + c++ case c == 'z': c = 'a' default: diff --git a/signature/policy_types.go b/signature/policy_types.go index 9e837452a7..96e91a0a9c 100644 --- a/signature/policy_types.go +++ b/signature/policy_types.go @@ -111,13 +111,24 @@ type prSignedBaseLayer struct { type prSigstoreSigned struct { prCommon - // KeyPath is a pathname to a local file containing the trusted key. Exactly one of KeyPath and KeyData must be specified. + // KeyPath is a pathname to a local file containing the trusted key. Exactly one of KeyPath, KeyData, Fulcio must be specified. KeyPath string `json:"keyPath,omitempty"` - // KeyData contains the trusted key, base64-encoded. Exactly one of KeyPath and KeyData must be specified. + // KeyData contains the trusted key, base64-encoded. Exactly one of KeyPath, KeyData, Fulcio must be specified. KeyData []byte `json:"keyData,omitempty"` // FIXME: Multiple public keys? - // FIXME: Support fulcio+rekor as an alternative. + // Fulcio specifies which Fulcio-generated certificates are accepted. Exactly one of KeyPath, KeyData, Fulcio must be specified. + // If Fulcio is specified, one of RekorPublicKeyPath or RekorPublicKeyData must be specified as well. + Fulcio PRSigstoreSignedFulcio `json:"fulcio,omitempty"` + + // RekorPublicKeyPath is a pathname to local file containing a public key of a Rekor server which must record acceptable signatures. + // If Fulcio is used, one of RekorPublicKeyPath or RekorPublicKeyData must be specified as well; otherwise it is optional + // (and Rekor inclusion is not required if a Rekor public key is not specified). + RekorPublicKeyPath string `json:"rekorPublicKeyPath,omitempty"` + // RekorPublicKeyPath contain a base64-encoded public key of a Rekor server which must record acceptable signatures. + // If Fulcio is used, one of RekorPublicKeyPath or RekorPublicKeyData must be specified as well; otherwise it is optional + // (and Rekor inclusion is not required if a Rekor public key is not specified). + RekorPublicKeyData []byte `json:"rekorPublicKeyData,omitempty"` // SignedIdentity specifies what image identity the signature must be claiming about the image. // Defaults to "matchRepoDigestOrExact" if not specified. @@ -125,6 +136,26 @@ type prSigstoreSigned struct { SignedIdentity PolicyReferenceMatch `json:"signedIdentity"` } +// PRSigstoreSignedFulcio contains Fulcio configuration options for a "sigstoreSigned" PolicyRequirement. +// This is a public type with a single private implementation. +type PRSigstoreSignedFulcio interface { + // toFulcioTrustRoot creates a fulcioTrustRoot from the input data. + // (This also prevents external implementations of this interface, ensuring that prSigstoreSignedFulcio is the only one.) + prepareTrustRoot() (*fulcioTrustRoot, error) +} + +// prSigstoreSignedFulcio collects Fulcio configuration options for prSigstoreSigned +type prSigstoreSignedFulcio struct { + // CAPath a path to a file containing accepted CA root certificates, in PEM format. Exactly one of CAPath and CAData must be specified. + CAPath string `json:"caPath,omitempty"` + // CAData contains accepted CA root certificates in PEM format, all of that base64-encoded. Exactly one of CAPath and CAData must be specified. + CAData []byte `json:"caData,omitempty"` + // OIDCIssuer specifies the expected OIDC issuer, recorded by Fulcio into the generated certificates. + OIDCIssuer string `json:"oidcIssuer,omitempty"` + // SubjectEmail specifies the expected email address of the authenticated OIDC identity, recorded by Fulcio into the generated certificates. + SubjectEmail string `json:"subjectEmail,omitempty"` +} + // PolicyReferenceMatch specifies a set of image identities accepted in PolicyRequirement. // The type is public, but its implementation is private. diff --git a/signature/signer/signer.go b/signature/signer/signer.go new file mode 100644 index 0000000000..73ae550aa5 --- /dev/null +++ b/signature/signer/signer.go @@ -0,0 +1,9 @@ +package signer + +import "github.com/containers/image/v5/internal/signer" + +// Signer is an object, possibly carrying state, that can be used by copy.Image to sign one or more container images. +// It can only be created from within the containers/image package; it can’t be implemented externally. +// +// The owner of a Signer must call Close() when done. +type Signer = signer.Signer diff --git a/signature/sigstore/copied.go b/signature/sigstore/copied.go index dbc03ec0a0..04e05fcb42 100644 --- a/signature/sigstore/copied.go +++ b/signature/sigstore/copied.go @@ -10,6 +10,7 @@ import ( "errors" "fmt" + "github.com/sigstore/sigstore/pkg/cryptoutils" "github.com/sigstore/sigstore/pkg/signature" "github.com/theupdateframework/go-tuf/encrypted" ) @@ -32,12 +33,12 @@ import ( // limitations under the License. const ( - // from sigstore/cosign/pkg/cosign.sigstorePrivateKeyPemType + // from sigstore/cosign/pkg/cosign.sigstorePrivateKeyPemType. sigstorePrivateKeyPemType = "ENCRYPTED COSIGN PRIVATE KEY" ) // from sigstore/cosign/pkg/cosign.loadPrivateKey -// FIXME: Do we need all of these key formats, and all of those +// FIXME: Do we need all of these key formats? func loadPrivateKey(key []byte, pass []byte) (signature.SignerVerifier, error) { // Decrypt first p, _ := pem.Decode(key) @@ -68,3 +69,31 @@ func loadPrivateKey(key []byte, pass []byte) (signature.SignerVerifier, error) { return nil, errors.New("unsupported key type") } } + +// simplified from sigstore/cosign/pkg/cosign.marshalKeyPair +// loadPrivateKey always requires a encryption, so this always requires a passphrase. +func marshalKeyPair(privateKey crypto.PrivateKey, publicKey crypto.PublicKey, password []byte) (_privateKey []byte, _publicKey []byte, err error) { + x509Encoded, err := x509.MarshalPKCS8PrivateKey(privateKey) + if err != nil { + return nil, nil, fmt.Errorf("x509 encoding private key: %w", err) + } + + encBytes, err := encrypted.Encrypt(x509Encoded, password) + if err != nil { + return nil, nil, err + } + + // store in PEM format + privBytes := pem.EncodeToMemory(&pem.Block{ + Bytes: encBytes, + Type: sigstorePrivateKeyPemType, + }) + + // Now do the public key + pubBytes, err := cryptoutils.MarshalPublicKeyToPEM(publicKey) + if err != nil { + return nil, nil, err + } + + return privBytes, pubBytes, nil +} diff --git a/signature/sigstore/fulcio/fulcio.go b/signature/sigstore/fulcio/fulcio.go new file mode 100644 index 0000000000..0e6746abb3 --- /dev/null +++ b/signature/sigstore/fulcio/fulcio.go @@ -0,0 +1,155 @@ +package fulcio + +import ( + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/sha256" + "crypto/x509" + "fmt" + "io" + "net/url" + + "github.com/containers/image/v5/internal/useragent" + "github.com/containers/image/v5/signature/sigstore/internal" + "github.com/sigstore/fulcio/pkg/api" + "github.com/sigstore/sigstore/pkg/oauth" + "github.com/sigstore/sigstore/pkg/oauthflow" + sigstoreSignature "github.com/sigstore/sigstore/pkg/signature" + "github.com/sirupsen/logrus" + "golang.org/x/oauth2" +) + +// setupSignerWithFulcio updates s with a certificate generated by fulcioURL based on oidcIDToken +func setupSignerWithFulcio(s *internal.SigstoreSigner, fulcioURL *url.URL, oidcIDToken *oauthflow.OIDCIDToken) error { + // ECDSA-P256 is the only interoperable algorithm per + // https://github.com/sigstore/cosign/blob/main/specs/SIGNATURE_SPEC.md#signature-schemes . + privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return fmt.Errorf("generating short-term private key: %w", err) + } + keyAlgorithm := "ecdsa" + // SHA-256 is opencontainers/go-digest.Canonical, thus the algorithm to use here as well per + // https://github.com/sigstore/cosign/blob/main/specs/SIGNATURE_SPEC.md#hashing-algorithms + signer, err := sigstoreSignature.LoadECDSASigner(privateKey, crypto.SHA256) + if err != nil { + return fmt.Errorf("initializing short-term private key: %w", err) + } + s.PrivateKey = signer + + logrus.Debugf("Requesting a certificate from Fulcio at %s", fulcioURL.Redacted()) + fulcioClient := api.NewClient(fulcioURL, api.WithUserAgent(useragent.DefaultUserAgent)) + // Sign the email address as part of the request + h := sha256.Sum256([]byte(oidcIDToken.Subject)) + keyOwnershipProof, err := ecdsa.SignASN1(rand.Reader, privateKey, h[:]) + if err != nil { + return fmt.Errorf("Error signing key ownership proof: %w", err) + } + publicKeyBytes, err := x509.MarshalPKIXPublicKey(&privateKey.PublicKey) + if err != nil { + return fmt.Errorf("converting public key to ASN.1: %w", err) + } + // Note that unlike most OAuth2 uses, this passes the ID token, not an access token. + // This is only secure if every Fulcio server has an individual client ID value + // = fulcioOIDCClientID, distinct from other Fulcio servers, + // that is embedded into the ID token’s "aud" field. + resp, err := fulcioClient.SigningCert(api.CertificateRequest{ + PublicKey: api.Key{ + Content: publicKeyBytes, + Algorithm: keyAlgorithm, + }, + SignedEmailAddress: keyOwnershipProof, + }, oidcIDToken.RawString) + if err != nil { + return fmt.Errorf("obtaining certificate from Fulcio: %w", err) + } + s.FulcioGeneratedCertificate = resp.CertPEM + s.FulcioGeneratedCertificateChain = resp.ChainPEM + // Cosign goes through an unmarshal/marshal roundtrip for Fulcio-generated certificates, let’s not do that. + s.SigningKeyOrCert = resp.CertPEM + return nil +} + +// WithFulcioAndPreexistingOIDCIDToken sets up signing to use a short-lived key and a Fulcio-issued certificate +// based on a caller-provided OIDC ID token. +func WithFulcioAndPreexistingOIDCIDToken(fulcioURL *url.URL, oidcIDToken string) internal.Option { + return func(s *internal.SigstoreSigner) error { + if s.PrivateKey != nil { + return fmt.Errorf("multiple private key sources specified when preparing to create sigstore signatures") + } + + // This adds dependencies even just to parse the token. We could possibly reimplement that, and split this variant + // into a subpackage without the OIDC dependencies… but really, is this going to be used in significantly different situations + // than the two interactive OIDC authentication workflows? + // + // Are there any widely used tools to manually obtain an ID token? Why would there be? + // For long-term usage, users provisioning a static OIDC credential might just as well provision an already-generated certificate + // or something like that. + logrus.Debugf("Using a statically-provided OIDC token") + staticTokenGetter := oauthflow.StaticTokenGetter{RawToken: oidcIDToken} + oidcIDToken, err := staticTokenGetter.GetIDToken(nil, oauth2.Config{}) + if err != nil { + return fmt.Errorf("parsing OIDC token: %w", err) + } + + return setupSignerWithFulcio(s, fulcioURL, oidcIDToken) + } +} + +// WithFulcioAndDeviceAuthorizationGrantOIDC sets up signing to use a short-lived key and a Fulcio-issued certificate +// based on an OIDC ID token obtained using a device authorization grant (RFC 8628). +// +// interactiveOutput must be directly accessible to a human user in real time (i.e. not be just a log file). +func WithFulcioAndDeviceAuthorizationGrantOIDC(fulcioURL *url.URL, oidcIssuerURL *url.URL, oidcClientID, oidcClientSecret string, + interactiveOutput io.Writer) internal.Option { + return func(s *internal.SigstoreSigner) error { + if s.PrivateKey != nil { + return fmt.Errorf("multiple private key sources specified when preparing to create sigstore signatures") + } + + logrus.Debugf("Starting OIDC device flow for issuer %s", oidcIssuerURL.Redacted()) + tokenGetter := oauthflow.NewDeviceFlowTokenGetterForIssuer(oidcIssuerURL.String()) + tokenGetter.MessagePrinter = func(s string) { + fmt.Fprintln(interactiveOutput, s) + } + oidcIDToken, err := oauthflow.OIDConnect(oidcIssuerURL.String(), oidcClientID, oidcClientSecret, "", tokenGetter) + if err != nil { + return fmt.Errorf("Error authenticating with OIDC: %w", err) + } + + return setupSignerWithFulcio(s, fulcioURL, oidcIDToken) + } +} + +// WithFulcioAndInterativeOIDC sets up signing to use a short-lived key and a Fulcio-issued certificate +// based on an interactively-obtained OIDC ID token. +// The token is obtained +// - directly using a browser, listening on localhost, automatically opening a browser to the OIDC issuer, +// to be redirected on localhost. (I.e. the current environment must allow launching a browser that connect back to the current process; +// either or both may be impossible in a container or a remote VM). +// - or by instructing the user to manually open a browser, obtain the OIDC code, and interactively input it as text. +// +// interactiveInput and interactiveOutput must both be directly operable by a human user in real time (i.e. not be just a log file). +func WithFulcioAndInteractiveOIDC(fulcioURL *url.URL, oidcIssuerURL *url.URL, oidcClientID, oidcClientSecret string, + interactiveInput io.Reader, interactiveOutput io.Writer) internal.Option { + return func(s *internal.SigstoreSigner) error { + if s.PrivateKey != nil { + return fmt.Errorf("multiple private key sources specified when preparing to create sigstore signatures") + } + + logrus.Debugf("Starting interactive OIDC authentication for issuer %s", oidcIssuerURL.Redacted()) + // This is intended to match oauthflow.DefaultIDTokenGetter, overriding only input/output + tokenGetter := &oauthflow.InteractiveIDTokenGetter{ + HTMLPage: oauth.InteractiveSuccessHTML, + Input: interactiveInput, + Output: interactiveOutput, + } + oidcIDToken, err := oauthflow.OIDConnect(oidcIssuerURL.String(), oidcClientID, oidcClientSecret, "", tokenGetter) + if err != nil { + return fmt.Errorf("Error authenticating with OIDC: %w", err) + } + + return setupSignerWithFulcio(s, fulcioURL, oidcIDToken) + } +} diff --git a/signature/sigstore/generate.go b/signature/sigstore/generate.go new file mode 100644 index 0000000000..77520c1232 --- /dev/null +++ b/signature/sigstore/generate.go @@ -0,0 +1,35 @@ +package sigstore + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" +) + +// GenerateKeyPairResult is a struct to ensure the private and public parts can not be confused by the caller. +type GenerateKeyPairResult struct { + PublicKey []byte + PrivateKey []byte +} + +// GenerateKeyPair generates a public/private key pair usable for signing images using the sigstore format, +// and returns key representations suitable for storing in long-term files (with the private key encrypted using the provided passphrase). +// The specific key kind (e.g. algorithm, size), as well as the file format, are unspecified by this API, +// and can change with best practices over time. +func GenerateKeyPair(passphrase []byte) (*GenerateKeyPairResult, error) { + // https://github.com/sigstore/cosign/blob/main/specs/SIGNATURE_SPEC.md#signature-schemes + // only requires ECDSA-P256 to be supported, so that’s what we must use. + rawKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + // Coverage: This can fail only if the randomness source fails + return nil, err + } + private, public, err := marshalKeyPair(rawKey, rawKey.Public(), passphrase) + if err != nil { + return nil, err + } + return &GenerateKeyPairResult{ + PublicKey: public, + PrivateKey: private, + }, nil +} diff --git a/signature/sigstore/generate_test.go b/signature/sigstore/generate_test.go new file mode 100644 index 0000000000..7861a92401 --- /dev/null +++ b/signature/sigstore/generate_test.go @@ -0,0 +1,64 @@ +package sigstore + +import ( + "context" + "os" + "path/filepath" + "testing" + + "github.com/containers/image/v5/docker/reference" + "github.com/containers/image/v5/internal/signature" + internalSigner "github.com/containers/image/v5/internal/signer" + "github.com/containers/image/v5/manifest" + "github.com/containers/image/v5/signature/internal" + "github.com/opencontainers/go-digest" + "github.com/sigstore/sigstore/pkg/cryptoutils" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGenerateKeyPair(t *testing.T) { + // Test that generation is possible, and the key can be used for signing. + testManifest := []byte("{}") + testDockerReference, err := reference.ParseNormalizedNamed("example.com/foo:notlatest") + require.NoError(t, err) + + passphrase := []byte("some passphrase") + keyPair, err := GenerateKeyPair(passphrase) + require.NoError(t, err) + + tmpDir := t.TempDir() + privateKeyFile := filepath.Join(tmpDir, "private.key") + err = os.WriteFile(privateKeyFile, keyPair.PrivateKey, 0600) + require.NoError(t, err) + + signer, err := NewSigner(WithPrivateKeyFile(privateKeyFile, passphrase)) + require.NoError(t, err) + sig0, err := internalSigner.SignImageManifest(context.Background(), signer, testManifest, testDockerReference) + require.NoError(t, err) + sig, ok := sig0.(signature.Sigstore) + require.True(t, ok) + + // It would be even more elegant to invoke the higher-level prSigstoreSigned code, + // but that is private. + publicKey, err := cryptoutils.UnmarshalPEMToPublicKey(keyPair.PublicKey) + require.NoError(t, err) + + _, err = internal.VerifySigstorePayload(publicKey, sig.UntrustedPayload(), + sig.UntrustedAnnotations()[signature.SigstoreSignatureAnnotationKey], + internal.SigstorePayloadAcceptanceRules{ + ValidateSignedDockerReference: func(ref string) error { + assert.Equal(t, "example.com/foo:notlatest", ref) + return nil + }, + ValidateSignedDockerManifestDigest: func(digest digest.Digest) error { + matches, err := manifest.MatchesDigest(testManifest, digest) + require.NoError(t, err) + assert.True(t, matches) + return nil + }, + }) + assert.NoError(t, err) + + // The failure paths are not obviously easy to reach. +} diff --git a/signature/sigstore/internal/signer.go b/signature/sigstore/internal/signer.go new file mode 100644 index 0000000000..c6258f408f --- /dev/null +++ b/signature/sigstore/internal/signer.go @@ -0,0 +1,95 @@ +package internal + +import ( + "bytes" + "context" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + + "github.com/containers/image/v5/docker/reference" + "github.com/containers/image/v5/internal/signature" + "github.com/containers/image/v5/manifest" + "github.com/containers/image/v5/signature/internal" + sigstoreSignature "github.com/sigstore/sigstore/pkg/signature" +) + +type Option func(*SigstoreSigner) error + +// SigstoreSigner is a signer.SignerImplementation implementation for sigstore signatures. +// It is initialized using various closures that implement Option, sadly over several subpackages, to decrease the +// dependency impact. +type SigstoreSigner struct { + PrivateKey sigstoreSignature.Signer // May be nil during initialization + SigningKeyOrCert []byte // For possible Rekor upload; always initialized together with PrivateKey + + // Fulcio results to include + FulcioGeneratedCertificate []byte // Or nil + FulcioGeneratedCertificateChain []byte // Or nil + + // Rekor state + RekorUploader func(ctx context.Context, keyOrCertBytes []byte, signatureBytes []byte, payloadBytes []byte) ([]byte, error) // Or nil +} + +// ProgressMessage returns a human-readable sentence that makes sense to write before starting to create a single signature. +func (s *SigstoreSigner) ProgressMessage() string { + return "Signing image using a sigstore signature" +} + +// SignImageManifest creates a new signature for manifest m as dockerReference. +func (s *SigstoreSigner) SignImageManifest(ctx context.Context, m []byte, dockerReference reference.Named) (signature.Signature, error) { + if s.PrivateKey == nil { + return nil, errors.New("internal error: nothing to sign with, should have been detected in NewSigner") + } + + if reference.IsNameOnly(dockerReference) { + return nil, fmt.Errorf("reference %s can’t be signed, it has neither a tag nor a digest", dockerReference.String()) + } + manifestDigest, err := manifest.Digest(m) + if err != nil { + return nil, err + } + // sigstore/cosign completely ignores dockerReference for actual policy decisions. + // They record the repo (but NOT THE TAG) in the value; without the tag we can’t detect version rollbacks. + // So, just do what simple signing does, and cosign won’t mind. + payloadData := internal.NewUntrustedSigstorePayload(manifestDigest, dockerReference.String()) + payloadBytes, err := json.Marshal(payloadData) + if err != nil { + return nil, err + } + + // github.com/sigstore/cosign/internal/pkg/cosign.payloadSigner uses signatureoptions.WithContext(), + // which seems to be not used by anything. So we don’t bother. + signatureBytes, err := s.PrivateKey.SignMessage(bytes.NewReader(payloadBytes)) + if err != nil { + return nil, fmt.Errorf("creating signature: %w", err) + } + base64Signature := base64.StdEncoding.EncodeToString(signatureBytes) + var rekorSETBytes []byte // = nil + if s.RekorUploader != nil { + set, err := s.RekorUploader(ctx, s.SigningKeyOrCert, signatureBytes, payloadBytes) + if err != nil { + return nil, err + } + rekorSETBytes = set + } + + annotations := map[string]string{ + signature.SigstoreSignatureAnnotationKey: base64Signature, + } + if s.FulcioGeneratedCertificate != nil { + annotations[signature.SigstoreCertificateAnnotationKey] = string(s.FulcioGeneratedCertificate) + } + if s.FulcioGeneratedCertificateChain != nil { + annotations[signature.SigstoreIntermediateCertificateChainAnnotationKey] = string(s.FulcioGeneratedCertificateChain) + } + if rekorSETBytes != nil { + annotations[signature.SigstoreSETAnnotationKey] = string(rekorSETBytes) + } + return signature.SigstoreFromComponents(signature.SigstoreSignatureMIMEType, payloadBytes, annotations), nil +} + +func (s *SigstoreSigner) Close() error { + return nil +} diff --git a/signature/sigstore/rekor/leveled_logger.go b/signature/sigstore/rekor/leveled_logger.go new file mode 100644 index 0000000000..f240d8cab8 --- /dev/null +++ b/signature/sigstore/rekor/leveled_logger.go @@ -0,0 +1,52 @@ +package rekor + +import ( + "fmt" + + "github.com/hashicorp/go-retryablehttp" + "github.com/sirupsen/logrus" +) + +// leveledLogger adapts our use of logrus to the expected go-retryablehttp.LeveledLogger interface. +type leveledLogger struct { + logger *logrus.Logger +} + +func leveledLoggerForLogrus(logger *logrus.Logger) retryablehttp.LeveledLogger { + return &leveledLogger{logger: logger} +} + +// log is the actual conversion implementation +func (l *leveledLogger) log(level logrus.Level, msg string, keysAndValues []any) { + fields := logrus.Fields{} + for i := 0; i < len(keysAndValues)-1; i += 2 { + key := keysAndValues[i] + keyString, isString := key.(string) + if !isString { + // It seems attractive to panic() here, but we might already be in a failure state, so let’s not make it worse + keyString = fmt.Sprintf("[Invalid LeveledLogger key %#v]", key) + } + fields[keyString] = keysAndValues[i+1] + } + l.logger.WithFields(fields).Log(level, msg) +} + +// Debug implements retryablehttp.LeveledLogger +func (l *leveledLogger) Debug(msg string, keysAndValues ...any) { + l.log(logrus.DebugLevel, msg, keysAndValues) +} + +// Error implements retryablehttp.LeveledLogger +func (l *leveledLogger) Error(msg string, keysAndValues ...any) { + l.log(logrus.ErrorLevel, msg, keysAndValues) +} + +// Info implements retryablehttp.LeveledLogger +func (l *leveledLogger) Info(msg string, keysAndValues ...any) { + l.log(logrus.InfoLevel, msg, keysAndValues) +} + +// Warn implements retryablehttp.LeveledLogger +func (l *leveledLogger) Warn(msg string, keysAndValues ...any) { + l.log(logrus.WarnLevel, msg, keysAndValues) +} diff --git a/signature/sigstore/rekor/rekor.go b/signature/sigstore/rekor/rekor.go new file mode 100644 index 0000000000..0236f0aabb --- /dev/null +++ b/signature/sigstore/rekor/rekor.go @@ -0,0 +1,160 @@ +package rekor + +import ( + "context" + "crypto/sha256" + "encoding/base64" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "net/url" + "strings" + + "github.com/containers/image/v5/signature/internal" + signerInternal "github.com/containers/image/v5/signature/sigstore/internal" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" + rekor "github.com/sigstore/rekor/pkg/client" + "github.com/sigstore/rekor/pkg/generated/client" + "github.com/sigstore/rekor/pkg/generated/client/entries" + "github.com/sigstore/rekor/pkg/generated/models" + "github.com/sirupsen/logrus" +) + +// WithRekor asks the generated signature to be uploaded to the specified Rekor server, +// and to include a log inclusion proof in the signature. +func WithRekor(rekorURL *url.URL) signerInternal.Option { + return func(s *signerInternal.SigstoreSigner) error { + logrus.Debugf("Using Rekor server at %s", rekorURL.Redacted()) + client, err := rekor.GetRekorClient(rekorURL.String(), + rekor.WithLogger(leveledLoggerForLogrus(logrus.StandardLogger()))) + if err != nil { + return fmt.Errorf("creating Rekor client: %w", err) + } + u := uploader{ + client: client, + } + s.RekorUploader = u.uploadKeyOrCert + return nil + } +} + +// uploader wraps a Rekor client, basically so that we can set RekorUploader to a method instead of an one-off closure. +type uploader struct { + client *client.Rekor +} + +// rekorEntryToSET converts a Rekor log entry into a sigstore “signed entry timestamp”. +func rekorEntryToSET(entry *models.LogEntryAnon) (internal.UntrustedRekorSET, error) { + // We could plausibly call entry.Validate() here; that mostly just uses unnecessary reflection instead of direct == nil checks. + // Right now the only extra validation .Validate() does is *entry.LogIndex >= 0 and a regex check on *entry.LogID; + // we don’t particularly care about either of these (notably signature verification only uses the Body value). + if entry.Verification == nil || entry.IntegratedTime == nil || entry.LogIndex == nil || entry.LogID == nil { + return internal.UntrustedRekorSET{}, fmt.Errorf("invalid Rekor entry (missing data): %#v", *entry) + } + bodyBase64, ok := entry.Body.(string) + if !ok { + return internal.UntrustedRekorSET{}, fmt.Errorf("unexpected Rekor entry body type: %#v", entry.Body) + } + body, err := base64.StdEncoding.DecodeString(bodyBase64) + if err != nil { + return internal.UntrustedRekorSET{}, fmt.Errorf("error parsing Rekor entry body: %w", err) + } + payloadJSON, err := internal.UntrustedRekorPayload{ + Body: body, + IntegratedTime: *entry.IntegratedTime, + LogIndex: *entry.LogIndex, + LogID: *entry.LogID, + }.MarshalJSON() + if err != nil { + return internal.UntrustedRekorSET{}, err + } + + return internal.UntrustedRekorSET{ + UntrustedSignedEntryTimestamp: entry.Verification.SignedEntryTimestamp, + UntrustedPayload: payloadJSON, + }, nil +} + +// uploadEntry ensures proposedEntry exists in Rekor (usually uploading it), and returns the resulting log entry. +func (u *uploader) uploadEntry(ctx context.Context, proposedEntry models.ProposedEntry) (models.LogEntry, error) { + params := entries.NewCreateLogEntryParamsWithContext(ctx) + params.SetProposedEntry(proposedEntry) + logrus.Debugf("Calling Rekor's CreateLogEntry") + resp, err := u.client.Entries.CreateLogEntry(params) + if err != nil { + // In ordinary operation, we should not get duplicate entries, because our payload contains a timestamp, + // so it is supposed to be unique; and the default key format, ECDSA p256, also contains a nonce. + // But conflicts can fairly easily happen during debugging and experimentation, so it pays to handle this. + var conflictErr *entries.CreateLogEntryConflict + if errors.As(err, &conflictErr) && conflictErr.Location != "" { + location := conflictErr.Location.String() + logrus.Debugf("CreateLogEntry reported a conflict, location = %s", location) + // We might be able to just GET the returned Location, but let’s use the generated API client. + // OTOH that requires us to hard-code the URI structure… + uuidDelimiter := strings.LastIndexByte(location, '/') + if uuidDelimiter != -1 { // Otherwise the URI is unexpected, and fall through to the bottom + uuid := location[uuidDelimiter+1:] + logrus.Debugf("Calling Rekor's NewGetLogEntryByUUIDParamsWithContext") + params2 := entries.NewGetLogEntryByUUIDParamsWithContext(ctx) + params2.SetEntryUUID(uuid) + resp2, err := u.client.Entries.GetLogEntryByUUID(params2) + if err != nil { + return nil, fmt.Errorf("Error re-loading previously-created log entry with UUID %s: %w", uuid, err) + } + return resp2.GetPayload(), nil + } + } + return nil, fmt.Errorf("Error uploading a log entry: %w", err) + } + return resp.GetPayload(), nil +} + +// uploadKeyOrCert integrates this code into sigstore/internal.Signer. +// Given components of the created signature, it returns a SET that should be added to the signature. +func (u *uploader) uploadKeyOrCert(ctx context.Context, keyOrCertBytes []byte, signatureBytes []byte, payloadBytes []byte) ([]byte, error) { + payloadHash := sha256.Sum256(payloadBytes) // HashedRecord only accepts SHA-256 + proposedEntry := models.Hashedrekord{ + APIVersion: swag.String(internal.HashedRekordV001APIVersion), + Spec: models.HashedrekordV001Schema{ + Data: &models.HashedrekordV001SchemaData{ + Hash: &models.HashedrekordV001SchemaDataHash{ + Algorithm: swag.String(models.HashedrekordV001SchemaDataHashAlgorithmSha256), + Value: swag.String(hex.EncodeToString(payloadHash[:])), + }, + }, + Signature: &models.HashedrekordV001SchemaSignature{ + Content: strfmt.Base64(signatureBytes), + PublicKey: &models.HashedrekordV001SchemaSignaturePublicKey{ + Content: strfmt.Base64(keyOrCertBytes), + }, + }, + }, + } + + uploadedPayload, err := u.uploadEntry(ctx, &proposedEntry) + if err != nil { + return nil, err + } + + if len(uploadedPayload) != 1 { + return nil, fmt.Errorf("expected 1 Rekor entry, got %d", len(uploadedPayload)) + } + var storedEntry *models.LogEntryAnon + // This “loop” extracts the single value from the uploadedPayload map. + for _, p := range uploadedPayload { + storedEntry = &p + break + } + + rekorBundle, err := rekorEntryToSET(storedEntry) + if err != nil { + return nil, err + } + rekorSETBytes, err := json.Marshal(rekorBundle) + if err != nil { + return nil, err + } + return rekorSETBytes, nil +} diff --git a/signature/sigstore/sign.go b/signature/sigstore/sign.go deleted file mode 100644 index daa6ab387c..0000000000 --- a/signature/sigstore/sign.go +++ /dev/null @@ -1,65 +0,0 @@ -package sigstore - -import ( - "bytes" - "encoding/base64" - "encoding/json" - "fmt" - "os" - - "github.com/containers/image/v5/docker/reference" - "github.com/containers/image/v5/internal/signature" - "github.com/containers/image/v5/manifest" - "github.com/containers/image/v5/signature/internal" - sigstoreSignature "github.com/sigstore/sigstore/pkg/signature" -) - -// SignDockerManifestWithPrivateKeyFileUnstable returns a signature for manifest as the specified dockerReference, -// using a private key and an optional passphrase. -// -// Yes, this returns an internal type, and should currently not be used outside of c/image. -// There is NO COMITTMENT TO STABLE API. -func SignDockerManifestWithPrivateKeyFileUnstable(m []byte, dockerReference reference.Named, privateKeyFile string, passphrase []byte) (signature.Sigstore, error) { - privateKeyPEM, err := os.ReadFile(privateKeyFile) - if err != nil { - return signature.Sigstore{}, fmt.Errorf("reading private key from %s: %w", privateKeyFile, err) - } - signer, err := loadPrivateKey(privateKeyPEM, passphrase) - if err != nil { - return signature.Sigstore{}, fmt.Errorf("initializing private key: %w", err) - } - - return signDockerManifest(m, dockerReference, signer) -} - -func signDockerManifest(m []byte, dockerReference reference.Named, signer sigstoreSignature.Signer) (signature.Sigstore, error) { - if reference.IsNameOnly(dockerReference) { - return signature.Sigstore{}, fmt.Errorf("reference %s can’t be signed, it has neither a tag nor a digest", dockerReference.String()) - } - manifestDigest, err := manifest.Digest(m) - if err != nil { - return signature.Sigstore{}, err - } - // sigstore/cosign completely ignores dockerReference for actual policy decisions. - // They record the repo (but NOT THE TAG) in the value; without the tag we can’t detect version rollbacks. - // So, just do what simple signing does, and cosign won’t mind. - payloadData := internal.NewUntrustedSigstorePayload(manifestDigest, dockerReference.String()) - payloadBytes, err := json.Marshal(payloadData) - if err != nil { - return signature.Sigstore{}, err - } - - // github.com/sigstore/cosign/internal/pkg/cosign.payloadSigner uses signatureoptions.WithContext(), - // which seems to be not used by anything. So we don’t bother. - signatureBytes, err := signer.SignMessage(bytes.NewReader(payloadBytes)) - if err != nil { - return signature.Sigstore{}, fmt.Errorf("creating signature: %w", err) - } - base64Signature := base64.StdEncoding.EncodeToString(signatureBytes) - - return signature.SigstoreFromComponents(signature.SigstoreSignatureMIMEType, - payloadBytes, - map[string]string{ - signature.SigstoreSignatureAnnotationKey: base64Signature, - }), nil -} diff --git a/signature/sigstore/signer.go b/signature/sigstore/signer.go new file mode 100644 index 0000000000..fb825ada9d --- /dev/null +++ b/signature/sigstore/signer.go @@ -0,0 +1,60 @@ +package sigstore + +import ( + "errors" + "fmt" + "os" + + internalSigner "github.com/containers/image/v5/internal/signer" + "github.com/containers/image/v5/signature/signer" + "github.com/containers/image/v5/signature/sigstore/internal" + "github.com/sigstore/sigstore/pkg/cryptoutils" +) + +type Option = internal.Option + +func WithPrivateKeyFile(file string, passphrase []byte) Option { + return func(s *internal.SigstoreSigner) error { + if s.PrivateKey != nil { + return fmt.Errorf("multiple private key sources specified when preparing to create sigstore signatures") + } + + if passphrase == nil { + return errors.New("private key passphrase not provided") + } + + privateKeyPEM, err := os.ReadFile(file) + if err != nil { + return fmt.Errorf("reading private key from %s: %w", file, err) + } + signerVerifier, err := loadPrivateKey(privateKeyPEM, passphrase) + if err != nil { + return fmt.Errorf("initializing private key: %w", err) + } + publicKey, err := signerVerifier.PublicKey() + if err != nil { + return fmt.Errorf("getting public key from private key: %w", err) + } + publicKeyPEM, err := cryptoutils.MarshalPublicKeyToPEM(publicKey) + if err != nil { + return fmt.Errorf("converting public key to PEM: %w", err) + } + s.PrivateKey = signerVerifier + s.SigningKeyOrCert = publicKeyPEM + return nil + } +} + +func NewSigner(opts ...Option) (*signer.Signer, error) { + s := internal.SigstoreSigner{} + for _, o := range opts { + if err := o(&s); err != nil { + return nil, err + } + } + if s.PrivateKey == nil { + return nil, errors.New("no private key source provided (neither a private key nor Fulcio) when preparing to create sigstore signatures") + } + + return internalSigner.NewSigner(&s), nil +} diff --git a/signature/simple.go b/signature/simple.go index 1ca571e5aa..56b222eda4 100644 --- a/signature/simple.go +++ b/signature/simple.go @@ -31,14 +31,14 @@ type Signature struct { // untrustedSignature is a parsed content of a signature. type untrustedSignature struct { - UntrustedDockerManifestDigest digest.Digest - UntrustedDockerReference string // FIXME: more precise type? - UntrustedCreatorID *string + untrustedDockerManifestDigest digest.Digest + untrustedDockerReference string // FIXME: more precise type? + untrustedCreatorID *string // This is intentionally an int64; the native JSON float64 type would allow to represent _some_ sub-second precision, // but not nearly enough (with current timestamp values, a single unit in the last place is on the order of hundreds of nanoseconds). // So, this is explicitly an int64, and we reject fractional values. If we did need more precise timestamps eventually, // we would add another field, UntrustedTimestampNS int64. - UntrustedTimestamp *int64 + untrustedTimestamp *int64 } // UntrustedSignatureInformation is information available in an untrusted signature. @@ -65,34 +65,35 @@ func newUntrustedSignature(dockerManifestDigest digest.Digest, dockerReference s creatorID := "atomic " + version.Version timestamp := time.Now().Unix() return untrustedSignature{ - UntrustedDockerManifestDigest: dockerManifestDigest, - UntrustedDockerReference: dockerReference, - UntrustedCreatorID: &creatorID, - UntrustedTimestamp: ×tamp, + untrustedDockerManifestDigest: dockerManifestDigest, + untrustedDockerReference: dockerReference, + untrustedCreatorID: &creatorID, + untrustedTimestamp: ×tamp, } } -// Compile-time check that untrustedSignature implements json.Marshaler +// A compile-time check that untrustedSignature and *untrustedSignature implements json.Marshaler +var _ json.Marshaler = untrustedSignature{} var _ json.Marshaler = (*untrustedSignature)(nil) // MarshalJSON implements the json.Marshaler interface. func (s untrustedSignature) MarshalJSON() ([]byte, error) { - if s.UntrustedDockerManifestDigest == "" || s.UntrustedDockerReference == "" { + if s.untrustedDockerManifestDigest == "" || s.untrustedDockerReference == "" { return nil, errors.New("Unexpected empty signature content") } - critical := map[string]interface{}{ + critical := map[string]any{ "type": signatureType, - "image": map[string]string{"docker-manifest-digest": s.UntrustedDockerManifestDigest.String()}, - "identity": map[string]string{"docker-reference": s.UntrustedDockerReference}, + "image": map[string]string{"docker-manifest-digest": s.untrustedDockerManifestDigest.String()}, + "identity": map[string]string{"docker-reference": s.untrustedDockerReference}, } - optional := map[string]interface{}{} - if s.UntrustedCreatorID != nil { - optional["creator"] = *s.UntrustedCreatorID + optional := map[string]any{} + if s.untrustedCreatorID != nil { + optional["creator"] = *s.untrustedCreatorID } - if s.UntrustedTimestamp != nil { - optional["timestamp"] = *s.UntrustedTimestamp + if s.untrustedTimestamp != nil { + optional["timestamp"] = *s.untrustedTimestamp } - signature := map[string]interface{}{ + signature := map[string]any{ "critical": critical, "optional": optional, } @@ -117,7 +118,7 @@ func (s *untrustedSignature) UnmarshalJSON(data []byte) error { // Splitting it into a separate function allows us to do the internal.JSONFormatError → InvalidSignatureError in a single place, the caller. func (s *untrustedSignature) strictUnmarshalJSON(data []byte) error { var critical, optional json.RawMessage - if err := internal.ParanoidUnmarshalJSONObjectExactFields(data, map[string]interface{}{ + if err := internal.ParanoidUnmarshalJSONObjectExactFields(data, map[string]any{ "critical": &critical, "optional": &optional, }); err != nil { @@ -127,7 +128,7 @@ func (s *untrustedSignature) strictUnmarshalJSON(data []byte) error { var creatorID string var timestamp float64 var gotCreatorID, gotTimestamp = false, false - if err := internal.ParanoidUnmarshalJSONObject(optional, func(key string) interface{} { + if err := internal.ParanoidUnmarshalJSONObject(optional, func(key string) any { switch key { case "creator": gotCreatorID = true @@ -136,26 +137,26 @@ func (s *untrustedSignature) strictUnmarshalJSON(data []byte) error { gotTimestamp = true return ×tamp default: - var ignore interface{} + var ignore any return &ignore } }); err != nil { return err } if gotCreatorID { - s.UntrustedCreatorID = &creatorID + s.untrustedCreatorID = &creatorID } if gotTimestamp { intTimestamp := int64(timestamp) if float64(intTimestamp) != timestamp { return internal.NewInvalidSignatureError("Field optional.timestamp is not is not an integer") } - s.UntrustedTimestamp = &intTimestamp + s.untrustedTimestamp = &intTimestamp } var t string var image, identity json.RawMessage - if err := internal.ParanoidUnmarshalJSONObjectExactFields(critical, map[string]interface{}{ + if err := internal.ParanoidUnmarshalJSONObjectExactFields(critical, map[string]any{ "type": &t, "image": &image, "identity": &identity, @@ -167,15 +168,15 @@ func (s *untrustedSignature) strictUnmarshalJSON(data []byte) error { } var digestString string - if err := internal.ParanoidUnmarshalJSONObjectExactFields(image, map[string]interface{}{ + if err := internal.ParanoidUnmarshalJSONObjectExactFields(image, map[string]any{ "docker-manifest-digest": &digestString, }); err != nil { return err } - s.UntrustedDockerManifestDigest = digest.Digest(digestString) + s.untrustedDockerManifestDigest = digest.Digest(digestString) - return internal.ParanoidUnmarshalJSONObjectExactFields(identity, map[string]interface{}{ - "docker-reference": &s.UntrustedDockerReference, + return internal.ParanoidUnmarshalJSONObjectExactFields(identity, map[string]any{ + "docker-reference": &s.untrustedDockerReference, }) } @@ -228,16 +229,16 @@ func verifyAndExtractSignature(mech SigningMechanism, unverifiedSignature []byte if err := json.Unmarshal(signed, &unmatchedSignature); err != nil { return nil, internal.NewInvalidSignatureError(err.Error()) } - if err := rules.validateSignedDockerManifestDigest(unmatchedSignature.UntrustedDockerManifestDigest); err != nil { + if err := rules.validateSignedDockerManifestDigest(unmatchedSignature.untrustedDockerManifestDigest); err != nil { return nil, err } - if err := rules.validateSignedDockerReference(unmatchedSignature.UntrustedDockerReference); err != nil { + if err := rules.validateSignedDockerReference(unmatchedSignature.untrustedDockerReference); err != nil { return nil, err } // signatureAcceptanceRules have accepted this value. return &Signature{ - DockerManifestDigest: unmatchedSignature.UntrustedDockerManifestDigest, - DockerReference: unmatchedSignature.UntrustedDockerReference, + DockerManifestDigest: unmatchedSignature.untrustedDockerManifestDigest, + DockerReference: unmatchedSignature.untrustedDockerReference, }, nil } @@ -268,14 +269,14 @@ func GetUntrustedSignatureInformationWithoutVerifying(untrustedSignatureBytes [] } var timestamp *time.Time // = nil - if untrustedDecodedContents.UntrustedTimestamp != nil { - ts := time.Unix(*untrustedDecodedContents.UntrustedTimestamp, 0) + if untrustedDecodedContents.untrustedTimestamp != nil { + ts := time.Unix(*untrustedDecodedContents.untrustedTimestamp, 0) timestamp = &ts } return &UntrustedSignatureInformation{ - UntrustedDockerManifestDigest: untrustedDecodedContents.UntrustedDockerManifestDigest, - UntrustedDockerReference: untrustedDecodedContents.UntrustedDockerReference, - UntrustedCreatorID: untrustedDecodedContents.UntrustedCreatorID, + UntrustedDockerManifestDigest: untrustedDecodedContents.untrustedDockerManifestDigest, + UntrustedDockerReference: untrustedDecodedContents.untrustedDockerReference, + UntrustedCreatorID: untrustedDecodedContents.untrustedCreatorID, UntrustedTimestamp: timestamp, UntrustedShortKeyIdentifier: shortKeyIdentifier, }, nil diff --git a/signature/simple_test.go b/signature/simple_test.go index a97fd6a639..313343e955 100644 --- a/signature/simple_test.go +++ b/signature/simple_test.go @@ -18,14 +18,14 @@ import ( func TestNewUntrustedSignature(t *testing.T) { timeBefore := time.Now() sig := newUntrustedSignature(TestImageManifestDigest, TestImageSignatureReference) - assert.Equal(t, TestImageManifestDigest, sig.UntrustedDockerManifestDigest) - assert.Equal(t, TestImageSignatureReference, sig.UntrustedDockerReference) - require.NotNil(t, sig.UntrustedCreatorID) - assert.Equal(t, "atomic "+version.Version, *sig.UntrustedCreatorID) - require.NotNil(t, sig.UntrustedTimestamp) + assert.Equal(t, TestImageManifestDigest, sig.untrustedDockerManifestDigest) + assert.Equal(t, TestImageSignatureReference, sig.untrustedDockerReference) + require.NotNil(t, sig.untrustedCreatorID) + assert.Equal(t, "atomic "+version.Version, *sig.untrustedCreatorID) + require.NotNil(t, sig.untrustedTimestamp) timeAfter := time.Now() - assert.True(t, timeBefore.Unix() <= *sig.UntrustedTimestamp) - assert.True(t, *sig.UntrustedTimestamp <= timeAfter.Unix()) + assert.True(t, timeBefore.Unix() <= *sig.untrustedTimestamp) + assert.True(t, *sig.untrustedTimestamp <= timeAfter.Unix()) } func TestMarshalJSON(t *testing.T) { @@ -47,17 +47,17 @@ func TestMarshalJSON(t *testing.T) { }{ { untrustedSignature{ - UntrustedDockerManifestDigest: "digest!@#", - UntrustedDockerReference: "reference#@!", - UntrustedCreatorID: &creatorID, - UntrustedTimestamp: ×tamp, + untrustedDockerManifestDigest: "digest!@#", + untrustedDockerReference: "reference#@!", + untrustedCreatorID: &creatorID, + untrustedTimestamp: ×tamp, }, "{\"critical\":{\"identity\":{\"docker-reference\":\"reference#@!\"},\"image\":{\"docker-manifest-digest\":\"digest!@#\"},\"type\":\"atomic container signature\"},\"optional\":{\"creator\":\"CREATOR\",\"timestamp\":1484683104}}", }, { untrustedSignature{ - UntrustedDockerManifestDigest: "digest!@#", - UntrustedDockerReference: "reference#@!", + untrustedDockerManifestDigest: "digest!@#", + untrustedDockerReference: "reference#@!", }, "{\"critical\":{\"identity\":{\"docker-reference\":\"reference#@!\"},\"image\":{\"docker-manifest-digest\":\"digest!@#\"},\"type\":\"atomic container signature\"},\"optional\":{}}", }, @@ -74,8 +74,8 @@ func TestMarshalJSON(t *testing.T) { } // Return the result of modifying validJSON with fn -func modifiedUntrustedSignatureJSON(t *testing.T, validJSON []byte, modifyFn func(mSI)) []byte { - var tmp mSI +func modifiedJSON(t *testing.T, validJSON []byte, modifyFn func(mSA)) []byte { + var tmp mSA err := json.Unmarshal(validJSON, &tmp) require.NoError(t, err) @@ -141,65 +141,65 @@ func TestUnmarshalJSON(t *testing.T) { assert.Equal(t, validSig, s) // Various ways to corrupt the JSON - breakFns := []func(mSI){ + breakFns := []func(mSA){ // A top-level field is missing - func(v mSI) { delete(v, "critical") }, - func(v mSI) { delete(v, "optional") }, + func(v mSA) { delete(v, "critical") }, + func(v mSA) { delete(v, "optional") }, // Extra top-level sub-object - func(v mSI) { v["unexpected"] = 1 }, + func(v mSA) { v["unexpected"] = 1 }, // "critical" not an object - func(v mSI) { v["critical"] = 1 }, + func(v mSA) { v["critical"] = 1 }, // "optional" not an object - func(v mSI) { v["optional"] = 1 }, + func(v mSA) { v["optional"] = 1 }, // A field of "critical" is missing - func(v mSI) { delete(x(v, "critical"), "type") }, - func(v mSI) { delete(x(v, "critical"), "image") }, - func(v mSI) { delete(x(v, "critical"), "identity") }, + func(v mSA) { delete(x(v, "critical"), "type") }, + func(v mSA) { delete(x(v, "critical"), "image") }, + func(v mSA) { delete(x(v, "critical"), "identity") }, // Extra field of "critical" - func(v mSI) { x(v, "critical")["unexpected"] = 1 }, + func(v mSA) { x(v, "critical")["unexpected"] = 1 }, // Invalid "type" - func(v mSI) { x(v, "critical")["type"] = 1 }, - func(v mSI) { x(v, "critical")["type"] = "unexpected" }, + func(v mSA) { x(v, "critical")["type"] = 1 }, + func(v mSA) { x(v, "critical")["type"] = "unexpected" }, // Invalid "image" object - func(v mSI) { x(v, "critical")["image"] = 1 }, - func(v mSI) { delete(x(v, "critical", "image"), "docker-manifest-digest") }, - func(v mSI) { x(v, "critical", "image")["unexpected"] = 1 }, + func(v mSA) { x(v, "critical")["image"] = 1 }, + func(v mSA) { delete(x(v, "critical", "image"), "docker-manifest-digest") }, + func(v mSA) { x(v, "critical", "image")["unexpected"] = 1 }, // Invalid "docker-manifest-digest" - func(v mSI) { x(v, "critical", "image")["docker-manifest-digest"] = 1 }, + func(v mSA) { x(v, "critical", "image")["docker-manifest-digest"] = 1 }, // Invalid "identity" object - func(v mSI) { x(v, "critical")["identity"] = 1 }, - func(v mSI) { delete(x(v, "critical", "identity"), "docker-reference") }, - func(v mSI) { x(v, "critical", "identity")["unexpected"] = 1 }, + func(v mSA) { x(v, "critical")["identity"] = 1 }, + func(v mSA) { delete(x(v, "critical", "identity"), "docker-reference") }, + func(v mSA) { x(v, "critical", "identity")["unexpected"] = 1 }, // Invalid "docker-reference" - func(v mSI) { x(v, "critical", "identity")["docker-reference"] = 1 }, + func(v mSA) { x(v, "critical", "identity")["docker-reference"] = 1 }, // Invalid "creator" - func(v mSI) { x(v, "optional")["creator"] = 1 }, + func(v mSA) { x(v, "optional")["creator"] = 1 }, // Invalid "timestamp" - func(v mSI) { x(v, "optional")["timestamp"] = "unexpected" }, - func(v mSI) { x(v, "optional")["timestamp"] = 0.5 }, // Fractional input + func(v mSA) { x(v, "optional")["timestamp"] = "unexpected" }, + func(v mSA) { x(v, "optional")["timestamp"] = 0.5 }, // Fractional input } for _, fn := range breakFns { - testJSON := modifiedUntrustedSignatureJSON(t, validJSON, fn) + testJSON := modifiedJSON(t, validJSON, fn) assertUnmarshalUntrustedSignatureFails(t, schemaLoader, testJSON) } // Modifications to unrecognized fields in "optional" are allowed and ignored - allowedModificationFns := []func(mSI){ + allowedModificationFns := []func(mSA){ // Add an optional field - func(v mSI) { x(v, "optional")["unexpected"] = 1 }, + func(v mSA) { x(v, "optional")["unexpected"] = 1 }, } for _, fn := range allowedModificationFns { - testJSON := modifiedUntrustedSignatureJSON(t, validJSON, fn) + testJSON := modifiedJSON(t, validJSON, fn) s := successfullyUnmarshalUntrustedSignature(t, schemaLoader, testJSON) assert.Equal(t, validSig, s) } // Optional fields can be missing validSig = untrustedSignature{ - UntrustedDockerManifestDigest: "digest!@#", - UntrustedDockerReference: "reference#@!", - UntrustedCreatorID: nil, - UntrustedTimestamp: nil, + untrustedDockerManifestDigest: "digest!@#", + untrustedDockerReference: "reference#@!", + untrustedCreatorID: nil, + untrustedTimestamp: nil, } validJSON, err = validSig.MarshalJSON() require.NoError(t, err) @@ -230,13 +230,13 @@ func TestSign(t *testing.T) { return nil }, validateSignedDockerReference: func(signedDockerReference string) error { - if signedDockerReference != sig.UntrustedDockerReference { + if signedDockerReference != sig.untrustedDockerReference { return errors.New("Unexpected signedDockerReference") } return nil }, validateSignedDockerManifestDigest: func(signedDockerManifestDigest digest.Digest) error { - if signedDockerManifestDigest != sig.UntrustedDockerManifestDigest { + if signedDockerManifestDigest != sig.untrustedDockerManifestDigest { return errors.New("Unexpected signedDockerManifestDigest") } return nil @@ -244,8 +244,8 @@ func TestSign(t *testing.T) { }) require.NoError(t, err) - assert.Equal(t, sig.UntrustedDockerManifestDigest, verified.DockerManifestDigest) - assert.Equal(t, sig.UntrustedDockerReference, verified.DockerReference) + assert.Equal(t, sig.untrustedDockerManifestDigest, verified.DockerManifestDigest) + assert.Equal(t, sig.untrustedDockerReference, verified.DockerReference) // Error creating blob to sign _, err = untrustedSignature{}.sign(mech, TestKeyFingerprint, "") diff --git a/signature/simplesigning/signer.go b/signature/simplesigning/signer.go new file mode 100644 index 0000000000..983bbb10b5 --- /dev/null +++ b/signature/simplesigning/signer.go @@ -0,0 +1,105 @@ +package simplesigning + +import ( + "context" + "errors" + "fmt" + "strings" + + "github.com/containers/image/v5/docker/reference" + internalSig "github.com/containers/image/v5/internal/signature" + internalSigner "github.com/containers/image/v5/internal/signer" + "github.com/containers/image/v5/signature" + "github.com/containers/image/v5/signature/signer" +) + +// simpleSigner is a signer.SignerImplementation implementation for simple signing signatures. +type simpleSigner struct { + mech signature.SigningMechanism + keyFingerprint string + passphrase string // "" if not provided. +} + +type Option func(*simpleSigner) error + +// WithKeyFingerprint returns an Option for NewSigner, specifying a key to sign with, using the provided GPG key fingerprint. +func WithKeyFingerprint(keyFingerprint string) Option { + return func(s *simpleSigner) error { + s.keyFingerprint = keyFingerprint + return nil + } +} + +// WithPassphrase returns an Option for NewSigner, specifying a passphrase for the private key. +// If this is not specified, the system may interactively prompt using a gpg-agent / pinentry. +func WithPassphrase(passphrase string) Option { + return func(s *simpleSigner) error { + // The gpgme implementation can’t use passphrase with \n; reject it here for consistent behavior. + if strings.Contains(passphrase, "\n") { + return errors.New("invalid passphrase: must not contain a line break") + } + s.passphrase = passphrase + return nil + } +} + +// NewSigner returns a signature.Signer which creates “simple signing” signatures using the user’s default +// GPG configuration ($GNUPGHOME / ~/.gnupg). +// +// The set of options must identify a key to sign with, probably using a WithKeyFingerprint. +// +// The caller must call Close() on the returned Signer. +func NewSigner(opts ...Option) (*signer.Signer, error) { + mech, err := signature.NewGPGSigningMechanism() + if err != nil { + return nil, fmt.Errorf("initializing GPG: %w", err) + } + succeeded := false + defer func() { + if !succeeded { + mech.Close() + } + }() + if err := mech.SupportsSigning(); err != nil { + return nil, fmt.Errorf("Signing not supported: %w", err) + } + + s := simpleSigner{ + mech: mech, + } + for _, o := range opts { + if err := o(&s); err != nil { + return nil, err + } + } + if s.keyFingerprint == "" { + return nil, errors.New("no key identity provided for simple signing") + } + // Ideally, we should look up (and unlock?) the key at this point already, but our current SigningMechanism API does not allow that. + + succeeded = true + return internalSigner.NewSigner(&s), nil +} + +// ProgressMessage returns a human-readable sentence that makes sense to write before starting to create a single signature. +func (s *simpleSigner) ProgressMessage() string { + return "Signing image using simple signing" +} + +// SignImageManifest creates a new signature for manifest m as dockerReference. +func (s *simpleSigner) SignImageManifest(ctx context.Context, m []byte, dockerReference reference.Named) (internalSig.Signature, error) { + if reference.IsNameOnly(dockerReference) { + return nil, fmt.Errorf("reference %s can’t be signed, it has neither a tag nor a digest", dockerReference.String()) + } + simpleSig, err := signature.SignDockerManifestWithOptions(m, dockerReference.String(), s.mech, s.keyFingerprint, &signature.SignOptions{ + Passphrase: s.passphrase, + }) + if err != nil { + return nil, err + } + return internalSig.SimpleSigningFromBlob(simpleSig), nil +} + +func (s *simpleSigner) Close() error { + return s.mech.Close() +} diff --git a/signature/simplesigning/signer_test.go b/signature/simplesigning/signer_test.go new file mode 100644 index 0000000000..0246c13c5a --- /dev/null +++ b/signature/simplesigning/signer_test.go @@ -0,0 +1,235 @@ +package simplesigning + +import ( + "context" + "os" + "testing" + + "github.com/containers/image/v5/docker/reference" + internalSig "github.com/containers/image/v5/internal/signature" + internalSigner "github.com/containers/image/v5/internal/signer" + "github.com/containers/image/v5/internal/testing/gpgagent" + "github.com/containers/image/v5/signature" + "github.com/opencontainers/go-digest" + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const ( + // testImageManifestDigest is the Docker manifest digest of "image.manifest.json" + testImageManifestDigest = digest.Digest("sha256:20bf21ed457b390829cdbeec8795a7bea1626991fda603e0d01b4e7f60427e55") + testGPGHomeDirectory = "./testdata" + // TestKeyFingerprint is the fingerprint of the private key in testGPGHomeDirectory. + testKeyFingerprint = "1D8230F6CDB6A06716E414C1DB72F2188BB46CC8" + // testKeyFingerprintWithPassphrase is the fingerprint of the private key with passphrase in testGPGHomeDirectory. + testKeyFingerprintWithPassphrase = "E3EB7611D815211F141946B5B0CDE60B42557346" + // testPassphrase is the passphrase for testKeyFingerprintWithPassphrase. + testPassphrase = "WithPassphrase123" +) + +// Ensure we don’t leave around GPG agent processes. +func TestMain(m *testing.M) { + code := m.Run() + if err := gpgagent.KillGPGAgent(testGPGHomeDirectory); err != nil { + logrus.Warnf("Error killing GPG agent: %v", err) + } + os.Exit(code) +} + +func TestNewSigner(t *testing.T) { + t.Setenv("GNUPGHOME", testGPGHomeDirectory) + + mech, err := signature.NewGPGSigningMechanism() + require.NoError(t, err) + defer mech.Close() + if err := mech.SupportsSigning(); err != nil { + t.Skipf("Signing not supported: %v", err) + } + + // An option causes an error + _, err = NewSigner(WithKeyFingerprint(testKeyFingerprintWithPassphrase), WithPassphrase("\n")) + assert.Error(t, err) + + // WithKeyFingerprint is missing + _, err = NewSigner(WithPassphrase("something")) + assert.Error(t, err) + + // A smoke test + s, err := NewSigner(WithKeyFingerprint(testKeyFingerprint)) + require.NoError(t, err) + err = s.Close() + assert.NoError(t, err) +} + +func TestSimpleSignerProgressMessage(t *testing.T) { + t.Setenv("GNUPGHOME", testGPGHomeDirectory) + + mech, err := signature.NewGPGSigningMechanism() + require.NoError(t, err) + defer mech.Close() + if err := mech.SupportsSigning(); err != nil { + t.Skipf("Signing not supported: %v", err) + } + + // Just a smoke test + s, err := NewSigner(WithKeyFingerprint(testKeyFingerprint)) + require.NoError(t, err) + defer func() { + err = s.Close() + assert.NoError(t, err) + }() + + _ = internalSigner.ProgressMessage(s) +} + +func TestSimpleSignerSignImageManifest(t *testing.T) { + t.Setenv("GNUPGHOME", testGPGHomeDirectory) + + mech, err := signature.NewGPGSigningMechanism() + require.NoError(t, err) + defer mech.Close() + if err := mech.SupportsSigning(); err != nil { + t.Skipf("Signing not supported: %v", err) + } + + err = gpgagent.KillGPGAgent(testGPGHomeDirectory) + require.NoError(t, err) + + manifest, err := os.ReadFile("../fixtures/image.manifest.json") + require.NoError(t, err) + testImageSignatureReference, err := reference.ParseNormalizedNamed("example.com/testing/manifest:notlatest") + require.NoError(t, err) + + // Failures to sign need to be tested in two parts: First the failures that involve the wrong passphrase, then failures that + // should manifest even with a valid passphrase or unlocked key (because the GPG agent is caching unlocked keys). + // Alternatively, we could be caling gpgagent.KillGPGAgent() all the time... + type failingCase struct { + name string + opts []Option + // NOTE: We DO NOT promise that things that don't fail during NewSigner won't start failing there. + // Actually we’d prefer failures to be identified early. This field only records current expected behavior, not the _desired_ end state. + creationFails bool + creationErrorContains string + manifest []byte + ref reference.Named + } + testFailure := func(c failingCase) { + s, err := NewSigner(c.opts...) + if c.creationFails { + assert.Error(t, err, c.name) + if c.creationErrorContains != "" { + assert.ErrorContains(t, err, c.creationErrorContains, c.name) + } + } else { + require.NoError(t, err, c.name) + defer s.Close() + + m := manifest + if c.manifest != nil { + m = c.manifest + } + _, err = internalSigner.SignImageManifest(context.Background(), s, m, c.ref) + assert.Error(t, err, c.name) + } + } + for _, c := range []failingCase{ + { + name: "Invalid passphrase", + opts: []Option{ + WithKeyFingerprint(testKeyFingerprintWithPassphrase), + WithPassphrase(testPassphrase + "\n"), + }, + creationFails: true, + creationErrorContains: "invalid passphrase", + ref: testImageSignatureReference, + }, + { + name: "Wrong passphrase", + opts: []Option{ + WithKeyFingerprint(testKeyFingerprintWithPassphrase), + WithPassphrase("wrong"), + }, + ref: testImageSignatureReference, + }, + { + name: "No passphrase", + opts: []Option{WithKeyFingerprint(testKeyFingerprintWithPassphrase)}, + ref: testImageSignatureReference, + }, + } { + testFailure(c) + } + + // Successful signing + for _, c := range []struct { + name string + fingerprint string + opts []Option + }{ + { + name: "No passphrase", + fingerprint: testKeyFingerprint, + }, + { + name: "With passphrase", + fingerprint: testKeyFingerprintWithPassphrase, + opts: []Option{WithPassphrase(testPassphrase)}, + }, + } { + s, err := NewSigner(append([]Option{WithKeyFingerprint(c.fingerprint)}, c.opts...)...) + require.NoError(t, err, c.name) + defer s.Close() + + sig, err := internalSigner.SignImageManifest(context.Background(), s, manifest, testImageSignatureReference) + require.NoError(t, err, c.name) + simpleSig, ok := sig.(internalSig.SimpleSigning) + require.True(t, ok) + + // FIXME FIXME: gpgme_op_sign with a passphrase succeeds, but somehow confuses the GPGME internal state + // so that gpgme_op_verify below never completes (it polls on an already closed FD). + // That’s probably a GPGME bug, and needs investigating and fixing, but it isn’t related to this “signer” implementation. + if len(c.opts) == 0 { + mech, err := signature.NewGPGSigningMechanism() + require.NoError(t, err) + defer mech.Close() + + verified, err := signature.VerifyDockerManifestSignature(simpleSig.UntrustedSignature(), manifest, testImageSignatureReference.String(), mech, c.fingerprint) + require.NoError(t, err) + assert.Equal(t, testImageSignatureReference.String(), verified.DockerReference) + assert.Equal(t, testImageManifestDigest, verified.DockerManifestDigest) + } + } + + invalidManifest, err := os.ReadFile("../fixtures/v2s1-invalid-signatures.manifest.json") + require.NoError(t, err) + invalidReference, err := reference.ParseNormalizedNamed("no-tag") + require.NoError(t, err) + for _, c := range []failingCase{ + { + name: "No key to sign with", + opts: nil, + creationFails: true, + }, + { + name: "Error computing Docker manifest", + opts: []Option{WithKeyFingerprint(testKeyFingerprint)}, + manifest: invalidManifest, + ref: testImageSignatureReference, + }, + { + name: "Invalid reference", + opts: []Option{WithKeyFingerprint(testKeyFingerprint)}, + ref: invalidReference, + }, + { + name: "Error signing", + opts: []Option{ + WithKeyFingerprint("this fingerprint doesn't exist"), + }, + ref: testImageSignatureReference, + }, + } { + testFailure(c) + } +} diff --git a/signature/simplesigning/testdata/.gitignore b/signature/simplesigning/testdata/.gitignore new file mode 100644 index 0000000000..2772b97f7d --- /dev/null +++ b/signature/simplesigning/testdata/.gitignore @@ -0,0 +1,6 @@ +/*.gpg~ +/.gpg-v21-migrated +/private-keys-v1.d +/random_seed +/gnupg_spawn_agent_sentinel.lock +/.#* diff --git a/signature/simplesigning/testdata/pubring.gpg b/signature/simplesigning/testdata/pubring.gpg new file mode 120000 index 0000000000..be16a533e9 --- /dev/null +++ b/signature/simplesigning/testdata/pubring.gpg @@ -0,0 +1 @@ +../../fixtures/pubring.gpg \ No newline at end of file diff --git a/signature/simplesigning/testdata/secring.gpg b/signature/simplesigning/testdata/secring.gpg new file mode 120000 index 0000000000..f8f8c2ba55 --- /dev/null +++ b/signature/simplesigning/testdata/secring.gpg @@ -0,0 +1 @@ +../../fixtures/secring.gpg \ No newline at end of file diff --git a/signature/simplesigning/testdata/trustdb.gpg b/signature/simplesigning/testdata/trustdb.gpg new file mode 120000 index 0000000000..e22f33aac9 --- /dev/null +++ b/signature/simplesigning/testdata/trustdb.gpg @@ -0,0 +1 @@ +../../fixtures/trustdb.gpg \ No newline at end of file diff --git a/storage/storage_dest.go b/storage/storage_dest.go index ae3bfa8fa4..d84d494933 100644 --- a/storage/storage_dest.go +++ b/storage/storage_dest.go @@ -21,6 +21,7 @@ import ( "github.com/containers/image/v5/internal/imagedestination/stubs" "github.com/containers/image/v5/internal/private" "github.com/containers/image/v5/internal/putblobdigest" + "github.com/containers/image/v5/internal/set" "github.com/containers/image/v5/internal/signature" "github.com/containers/image/v5/internal/tmpdir" "github.com/containers/image/v5/manifest" @@ -34,6 +35,7 @@ import ( digest "github.com/opencontainers/go-digest" imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" "github.com/sirupsen/logrus" + "golang.org/x/exp/slices" ) var ( @@ -157,7 +159,7 @@ func (s *storageImageDestination) computeNextBlobCacheFile() string { // to any other readers for download using the supplied digest. // If stream.Read() at any time, ESPECIALLY at end of input, returns an error, PutBlob MUST 1) fail, and 2) delete any data stored so far. func (s *storageImageDestination) PutBlobWithOptions(ctx context.Context, stream io.Reader, blobinfo types.BlobInfo, options private.PutBlobOptions) (types.BlobInfo, error) { - info, err := s.putBlobToPendingFile(ctx, stream, blobinfo, &options) + info, err := s.putBlobToPendingFile(stream, blobinfo, &options) if err != nil { return info, err } @@ -171,7 +173,7 @@ func (s *storageImageDestination) PutBlobWithOptions(ctx context.Context, stream // putBlobToPendingFile implements ImageDestination.PutBlobWithOptions, storing stream into an on-disk file. // The caller must arrange the blob to be eventually committed using s.commitLayer(). -func (s *storageImageDestination) putBlobToPendingFile(ctx context.Context, stream io.Reader, blobinfo types.BlobInfo, options *private.PutBlobOptions) (types.BlobInfo, error) { +func (s *storageImageDestination) putBlobToPendingFile(stream io.Reader, blobinfo types.BlobInfo, options *private.PutBlobOptions) (types.BlobInfo, error) { // Stores a layer or data blob in our temporary directory, checking that any information // in the blobinfo matches the incoming data. errorBlobInfo := types.BlobInfo{ @@ -201,7 +203,7 @@ func (s *storageImageDestination) putBlobToPendingFile(ctx context.Context, stre diffID := digest.Canonical.Digester() // Copy the data to the file. - // TODO: This can take quite some time, and should ideally be cancellable using ctx.Done(). + // TODO: This can take quite some time, and should ideally be cancellable using context.Context. _, err = io.Copy(diffID.Hash(), decompressed) decompressed.Close() if err != nil { @@ -242,7 +244,7 @@ type zstdFetcher struct { // GetBlobAt converts from chunked.GetBlobAt to BlobChunkAccessor.GetBlobAt. func (f *zstdFetcher) GetBlobAt(chunks []chunked.ImageSourceChunk) (chan io.ReadCloser, chan error, error) { - var newChunks []private.ImageSourceChunk + newChunks := make([]private.ImageSourceChunk, 0, len(chunks)) for _, v := range chunks { i := private.ImageSourceChunk{ Offset: v.Offset, @@ -300,7 +302,7 @@ func (s *storageImageDestination) PutBlobPartial(ctx context.Context, chunkAcces // reflected in the manifest that will be written. // If the transport can not reuse the requested blob, TryReusingBlob returns (false, {}, nil); it returns a non-nil error only on an unexpected failure. func (s *storageImageDestination) TryReusingBlobWithOptions(ctx context.Context, blobinfo types.BlobInfo, options private.TryReusingBlobOptions) (bool, types.BlobInfo, error) { - reused, info, err := s.tryReusingBlobAsPending(ctx, blobinfo, &options) + reused, info, err := s.tryReusingBlobAsPending(blobinfo, &options) if err != nil || !reused || options.LayerIndex == nil { return reused, info, err } @@ -310,7 +312,7 @@ func (s *storageImageDestination) TryReusingBlobWithOptions(ctx context.Context, // tryReusingBlobAsPending implements TryReusingBlobWithOptions, filling s.blobDiffIDs and other metadata. // The caller must arrange the blob to be eventually committed using s.commitLayer(). -func (s *storageImageDestination) tryReusingBlobAsPending(ctx context.Context, blobinfo types.BlobInfo, options *private.TryReusingBlobOptions) (bool, types.BlobInfo, error) { +func (s *storageImageDestination) tryReusingBlobAsPending(blobinfo types.BlobInfo, options *private.TryReusingBlobOptions) (bool, types.BlobInfo, error) { // lock the entire method as it executes fairly quickly s.lock.Lock() defer s.lock.Unlock() @@ -795,14 +797,14 @@ func (s *storageImageDestination) Commit(ctx context.Context, unparsedToplevel t // Add the non-layer blobs as data items. Since we only share layers, they should all be in files, so // we just need to screen out the ones that are actually layers to get the list of non-layers. - dataBlobs := make(map[digest.Digest]struct{}) + dataBlobs := set.New[digest.Digest]() for blob := range s.filenames { - dataBlobs[blob] = struct{}{} + dataBlobs.Add(blob) } for _, layerBlob := range layerBlobs { - delete(dataBlobs, layerBlob.Digest) + dataBlobs.Delete(layerBlob.Digest) } - for blob := range dataBlobs { + for _, blob := range dataBlobs.Values() { v, err := os.ReadFile(s.filenames[blob]) if err != nil { return fmt.Errorf("copying non-layer blob %q to image: %w", blob, err) @@ -883,9 +885,7 @@ func (s *storageImageDestination) PutManifest(ctx context.Context, manifestBlob if err != nil { return err } - newBlob := make([]byte, len(manifestBlob)) - copy(newBlob, manifestBlob) - s.manifest = newBlob + s.manifest = slices.Clone(manifestBlob) s.manifestDigest = digest return nil } diff --git a/storage/storage_image.go b/storage/storage_image.go index 9f16dd334d..ac09f3dbbc 100644 --- a/storage/storage_image.go +++ b/storage/storage_image.go @@ -43,7 +43,7 @@ func (s *storageImageCloser) Size() (int64, error) { // newImage creates an image that also knows its size func newImage(ctx context.Context, sys *types.SystemContext, s storageReference) (types.ImageCloser, error) { - src, err := newImageSource(ctx, sys, s) + src, err := newImageSource(sys, s) if err != nil { return nil, err } diff --git a/storage/storage_reference.go b/storage/storage_reference.go index dbb9804a6b..49f7d03c85 100644 --- a/storage/storage_reference.go +++ b/storage/storage_reference.go @@ -14,6 +14,7 @@ import ( "github.com/containers/storage" digest "github.com/opencontainers/go-digest" "github.com/sirupsen/logrus" + "golang.org/x/exp/slices" ) // A storageReference holds an arbitrary name and/or an ID, which is a 32-byte @@ -52,17 +53,15 @@ func newReference(transport storageTransport, named reference.Named, id string) // imageMatchesRepo returns true iff image.Names contains an element with the same repo as ref func imageMatchesRepo(image *storage.Image, ref reference.Named) bool { repo := ref.Name() - for _, name := range image.Names { - if named, err := reference.ParseNormalizedNamed(name); err == nil { - if named.Name() == repo { - return true - } + return slices.ContainsFunc(image.Names, func(name string) bool { + if named, err := reference.ParseNormalizedNamed(name); err == nil && named.Name() == repo { + return true } - } - return false + return false + }) } -// multiArchImageMatchesSystemContext returns true if if the passed-in image both contains a +// multiArchImageMatchesSystemContext returns true if the passed-in image both contains a // multi-arch manifest that matches the passed-in digest, and the image is the per-platform // image instance that matches sys. // @@ -170,11 +169,9 @@ func (s *storageReference) resolveImage(sys *types.SystemContext) (*storage.Imag // sake of older consumers that don't know there's a whole list in there now. if s.named != nil { if digested, ok := s.named.(reference.Digested); ok { - for _, digest := range loadedImage.Digests { - if digest == digested.Digest() { - loadedImage.Digest = digest - break - } + digest := digested.Digest() + if slices.Contains(loadedImage.Digests, digest) { + loadedImage.Digest = digest } } } @@ -207,10 +204,10 @@ func (s storageReference) StringWithinTransport() string { } res := "[" + s.transport.store.GraphDriverName() + "@" + s.transport.store.GraphRoot() + "+" + s.transport.store.RunRoot() + optionsList + "]" if s.named != nil { - res = res + s.named.String() + res += s.named.String() } if s.id != "" { - res = res + "@" + s.id + res += "@" + s.id } return res } @@ -218,10 +215,10 @@ func (s storageReference) StringWithinTransport() string { func (s storageReference) PolicyConfigurationIdentity() string { res := "[" + s.transport.store.GraphDriverName() + "@" + s.transport.store.GraphRoot() + "]" if s.named != nil { - res = res + s.named.String() + res += s.named.String() } if s.id != "" { - res = res + "@" + s.id + res += "@" + s.id } return res } @@ -280,7 +277,7 @@ func (s storageReference) DeleteImage(ctx context.Context, sys *types.SystemCont } func (s storageReference) NewImageSource(ctx context.Context, sys *types.SystemContext) (types.ImageSource, error) { - return newImageSource(ctx, sys, s) + return newImageSource(sys, s) } func (s storageReference) NewImageDestination(ctx context.Context, sys *types.SystemContext) (types.ImageDestination, error) { diff --git a/storage/storage_src.go b/storage/storage_src.go index d4288dade5..c2a843c841 100644 --- a/storage/storage_src.go +++ b/storage/storage_src.go @@ -23,7 +23,6 @@ import ( "github.com/containers/image/v5/types" "github.com/containers/storage" "github.com/containers/storage/pkg/archive" - "github.com/containers/storage/pkg/ioutils" digest "github.com/opencontainers/go-digest" imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" "github.com/sirupsen/logrus" @@ -45,7 +44,7 @@ type storageImageSource struct { } // newImageSource sets up an image for reading. -func newImageSource(ctx context.Context, sys *types.SystemContext, imageRef storageReference) (*storageImageSource, error) { +func newImageSource(sys *types.SystemContext, imageRef storageReference) (*storageImageSource, error) { // First, locate the image. img, err := imageRef.resolveImage(sys) if err != nil { @@ -89,14 +88,37 @@ func (s *storageImageSource) Close() error { // The Digest field in BlobInfo is guaranteed to be provided, Size may be -1 and MediaType may be optionally provided. // May update BlobInfoCache, preferably after it knows for certain that a blob truly exists at a specific location. func (s *storageImageSource) GetBlob(ctx context.Context, info types.BlobInfo, cache types.BlobInfoCache) (rc io.ReadCloser, n int64, err error) { - if info.Digest == image.GzippedEmptyLayerDigest { + // We need a valid digest value. + digest := info.Digest + err = digest.Validate() + if err != nil { + return nil, 0, err + } + + if digest == image.GzippedEmptyLayerDigest { return io.NopCloser(bytes.NewReader(image.GzippedEmptyLayer)), int64(len(image.GzippedEmptyLayer)), nil } + // Check if the blob corresponds to a diff that was used to initialize any layers. Our + // callers should try to retrieve layers using their uncompressed digests, so no need to + // check if they're using one of the compressed digests, which we can't reproduce anyway. + layers, _ := s.imageRef.transport.store.LayersByUncompressedDigest(digest) + + // If it's not a layer, then it must be a data item. + if len(layers) == 0 { + b, err := s.imageRef.transport.store.ImageBigData(s.image.ID, digest.String()) + if err != nil { + return nil, 0, err + } + r := bytes.NewReader(b) + logrus.Debugf("exporting opaque data as blob %q", digest.String()) + return io.NopCloser(r), int64(r.Len()), nil + } + // NOTE: the blob is first written to a temporary file and subsequently // closed. The intention is to keep the time we own the storage lock // as short as possible to allow other processes to access the storage. - rc, n, _, err = s.getBlobAndLayerID(info) + rc, n, _, err = s.getBlobAndLayerID(digest, layers) if err != nil { return nil, 0, err } @@ -106,53 +128,40 @@ func (s *storageImageSource) GetBlob(ctx context.Context, info types.BlobInfo, c if err != nil { return nil, 0, err } + success := false + defer func() { + if !success { + tmpFile.Close() + } + }() + // On Unix and modern Windows (2022 at least) we can eagerly unlink the file to ensure it's automatically + // cleaned up on process termination (or if the caller forgets to invoke Close()) + if err := os.Remove(tmpFile.Name()); err != nil { + return nil, 0, err + } if _, err := io.Copy(tmpFile, rc); err != nil { return nil, 0, err } - - if _, err := tmpFile.Seek(0, 0); err != nil { + if _, err := tmpFile.Seek(0, io.SeekStart); err != nil { return nil, 0, err } - wrapper := ioutils.NewReadCloserWrapper(tmpFile, func() error { - defer os.Remove(tmpFile.Name()) - return tmpFile.Close() - }) - - return wrapper, n, err + success = true + return tmpFile, n, nil } // getBlobAndLayer reads the data blob or filesystem layer which matches the digest and size, if given. -func (s *storageImageSource) getBlobAndLayerID(info types.BlobInfo) (rc io.ReadCloser, n int64, layerID string, err error) { +func (s *storageImageSource) getBlobAndLayerID(digest digest.Digest, layers []storage.Layer) (rc io.ReadCloser, n int64, layerID string, err error) { var layer storage.Layer var diffOptions *storage.DiffOptions - // We need a valid digest value. - err = info.Digest.Validate() - if err != nil { - return nil, -1, "", err - } - // Check if the blob corresponds to a diff that was used to initialize any layers. Our - // callers should try to retrieve layers using their uncompressed digests, so no need to - // check if they're using one of the compressed digests, which we can't reproduce anyway. - layers, _ := s.imageRef.transport.store.LayersByUncompressedDigest(info.Digest) - // If it's not a layer, then it must be a data item. - if len(layers) == 0 { - b, err := s.imageRef.transport.store.ImageBigData(s.image.ID, info.Digest.String()) - if err != nil { - return nil, -1, "", err - } - r := bytes.NewReader(b) - logrus.Debugf("exporting opaque data as blob %q", info.Digest.String()) - return io.NopCloser(r), int64(r.Len()), "", nil - } // Step through the list of matching layers. Tests may want to verify that if we have multiple layers // which claim to have the same contents, that we actually do have multiple layers, otherwise we could // just go ahead and use the first one every time. s.getBlobMutex.Lock() - i := s.layerPosition[info.Digest] - s.layerPosition[info.Digest] = i + 1 + i := s.layerPosition[digest] + s.layerPosition[digest] = i + 1 s.getBlobMutex.Unlock() if len(layers) > 0 { layer = layers[i%len(layers)] @@ -168,7 +177,7 @@ func (s *storageImageSource) getBlobAndLayerID(info types.BlobInfo) (rc io.ReadC } else { n = layer.UncompressedSize } - logrus.Debugf("exporting filesystem layer %q without compression for blob %q", layer.ID, info.Digest) + logrus.Debugf("exporting filesystem layer %q without compression for blob %q", layer.ID, digest) rc, err = s.imageRef.transport.store.Diff("", layer.ID, diffOptions) if err != nil { return nil, -1, "", err @@ -177,7 +186,7 @@ func (s *storageImageSource) getBlobAndLayerID(info types.BlobInfo) (rc io.ReadC } // GetManifest() reads the image's manifest. -func (s *storageImageSource) GetManifest(ctx context.Context, instanceDigest *digest.Digest) (manifestBlob []byte, MIMEType string, err error) { +func (s *storageImageSource) GetManifest(ctx context.Context, instanceDigest *digest.Digest) (manifestBlob []byte, mimeType string, err error) { if instanceDigest != nil { key := manifestBigDataKey(*instanceDigest) blob, err := s.imageRef.transport.store.ImageBigData(s.image.ID, key) diff --git a/storage/storage_test.go b/storage/storage_test.go index 6bbab68e44..e5f5467e28 100644 --- a/storage/storage_test.go +++ b/storage/storage_test.go @@ -21,8 +21,8 @@ import ( "testing" "time" + imanifest "github.com/containers/image/v5/internal/manifest" "github.com/containers/image/v5/internal/private" - imanifest "github.com/containers/image/v5/manifest" "github.com/containers/image/v5/pkg/blobinfocache/memory" "github.com/containers/image/v5/types" "github.com/containers/storage" @@ -33,6 +33,7 @@ import ( ddigest "github.com/opencontainers/go-digest" "github.com/sirupsen/logrus" "github.com/stretchr/testify/require" + "golang.org/x/exp/slices" ) var ( @@ -192,13 +193,8 @@ func TestParseWithGraphDriverOptions(t *testing.T) { t.Fatalf("ParseReference returned a reference from transport %s, not one of ours", ref2.Transport().Name()) } parsedOptions := sref.transport.store.GraphOptions() - if len(parsedOptions) != len(optionList) { - t.Fatalf("Lost options between %v and %v", optionList, parsedOptions) - } - for i := range optionList { - if parsedOptions[i] != optionList[i] { - t.Fatalf("Mismatched option %d: %v and %v", i, optionList[i], parsedOptions[i]) - } + if !slices.Equal(parsedOptions, optionList) { + t.Fatalf("Mismatched options: %#v and %#v", optionList, parsedOptions) } } } @@ -475,7 +471,7 @@ func TestWriteRead(t *testing.T) { if err != nil { t.Fatalf("GetManifest(%q) with an instanceDigest is supposed to succeed", ref.StringWithinTransport()) } - if string(retrieved) != string(manifest) { + if string(retrieved) != manifest { t.Fatalf("GetManifest(%q) with an instanceDigest retrieved a different manifest", ref.StringWithinTransport()) } sigs, err := src.GetSignatures(context.Background(), nil) @@ -603,7 +599,7 @@ func TestDuplicateName(t *testing.T) { } digest, _, size, blob = makeLayer(t, archive.Gzip) if _, err := dest.PutBlob(context.Background(), bytes.NewBuffer(blob), types.BlobInfo{ - Size: int64(size), + Size: size, Digest: digest, }, cache, false); err != nil { t.Fatalf("Error saving randomly-generated layer to destination, second pass: %v", err) @@ -702,7 +698,7 @@ func TestDuplicateID(t *testing.T) { } digest, _, size, blob = makeLayer(t, archive.Gzip) if _, err := dest.PutBlob(context.Background(), bytes.NewBuffer(blob), types.BlobInfo{ - Size: int64(size), + Size: size, Digest: digest, }, cache, false); err != nil { t.Fatalf("Error saving randomly-generated layer to destination, second pass: %v", err) @@ -804,7 +800,7 @@ func TestDuplicateNameID(t *testing.T) { } digest, _, size, blob = makeLayer(t, archive.Gzip) if _, err := dest.PutBlob(context.Background(), bytes.NewBuffer(blob), types.BlobInfo{ - Size: int64(size), + Size: size, Digest: digest, }, cache, false); err != nil { t.Fatalf("Error saving randomly-generated layer to destination, second pass: %v", err) @@ -1098,7 +1094,8 @@ func TestDuplicateBlob(t *testing.T) { t.Fatalf("LayerInfosForCopy() returned error %v", err) } for _, layerInfo := range layersInfo { - rc, _, layerID, err := source.getBlobAndLayerID(layerInfo) + digestLayers, _ := source.imageRef.transport.store.LayersByUncompressedDigest(layerInfo.Digest) + rc, _, layerID, err := source.getBlobAndLayerID(layerInfo.Digest, digestLayers) if err != nil { t.Fatalf("getBlobAndLayerID(%q) returned error %v", layerInfo.Digest, err) } diff --git a/storage/storage_transport.go b/storage/storage_transport.go index 104e9d8cca..58ba3ee651 100644 --- a/storage/storage_transport.go +++ b/storage/storage_transport.go @@ -234,35 +234,30 @@ func (s *storageTransport) ParseReference(reference string) (types.ImageReferenc reference = reference[closeIndex+1:] // Peel off a "driver@" from the start. driverInfo := "" - driverSplit := strings.SplitN(storeSpec, "@", 2) - if len(driverSplit) != 2 { + driverPart1, driverPart2, gotDriver := strings.Cut(storeSpec, "@") + if !gotDriver { + storeSpec = driverPart1 if storeSpec == "" { return nil, ErrInvalidReference } } else { - driverInfo = driverSplit[0] + driverInfo = driverPart1 if driverInfo == "" { return nil, ErrInvalidReference } - storeSpec = driverSplit[1] + storeSpec = driverPart2 if storeSpec == "" { return nil, ErrInvalidReference } } // Peel off a ":options" from the end. var options []string - optionsSplit := strings.SplitN(storeSpec, ":", 2) - if len(optionsSplit) == 2 { - options = strings.Split(optionsSplit[1], ",") - storeSpec = optionsSplit[0] + storeSpec, optionsPart, gotOptions := strings.Cut(storeSpec, ":") + if gotOptions { + options = strings.Split(optionsPart, ",") } // Peel off a "+runroot" from the new end. - runRootInfo := "" - runRootSplit := strings.SplitN(storeSpec, "+", 2) - if len(runRootSplit) == 2 { - runRootInfo = runRootSplit[1] - storeSpec = runRootSplit[0] - } + storeSpec, runRootInfo, _ := strings.Cut(storeSpec, "+") // runRootInfo is "" if there is no "+" // The rest is our graph root. rootInfo := storeSpec // Check that any paths are absolute paths. diff --git a/tarball/tarball_reference.go b/tarball/tarball_reference.go index 690067ec3d..d5578d9e8a 100644 --- a/tarball/tarball_reference.go +++ b/tarball/tarball_reference.go @@ -9,8 +9,8 @@ import ( "github.com/containers/image/v5/docker/reference" "github.com/containers/image/v5/internal/image" "github.com/containers/image/v5/types" - imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" + "golang.org/x/exp/maps" ) // ConfigUpdater is an interface that ImageReferences for "tarball" images also @@ -35,9 +35,7 @@ func (r *tarballReference) ConfigUpdate(config imgspecv1.Image, annotations map[ if r.annotations == nil { r.annotations = make(map[string]string) } - for k, v := range annotations { - r.annotations[k] = v - } + maps.Copy(r.annotations, annotations) return nil } @@ -73,7 +71,7 @@ func (r *tarballReference) NewImage(ctx context.Context, sys *types.SystemContex func (r *tarballReference) DeleteImage(ctx context.Context, sys *types.SystemContext) error { for _, filename := range r.filenames { if err := os.Remove(filename); err != nil && !os.IsNotExist(err) { - return fmt.Errorf("error removing %q: %v", filename, err) + return fmt.Errorf("error removing %q: %w", filename, err) } } return nil diff --git a/tarball/tarball_src.go b/tarball/tarball_src.go index 1dc4c3ad94..0fb3a83934 100644 --- a/tarball/tarball_src.go +++ b/tarball/tarball_src.go @@ -18,6 +18,8 @@ import ( digest "github.com/opencontainers/go-digest" imgspecs "github.com/opencontainers/image-spec/specs-go" imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" + "golang.org/x/exp/maps" + "golang.org/x/exp/slices" ) type tarballImageSource struct { @@ -62,13 +64,13 @@ func (r *tarballReference) NewImageSource(ctx context.Context, sys *types.System } else { file, err = os.Open(filename) if err != nil { - return nil, fmt.Errorf("error opening %q for reading: %v", filename, err) + return nil, fmt.Errorf("error opening %q for reading: %w", filename, err) } defer file.Close() reader = file fileinfo, err := file.Stat() if err != nil { - return nil, fmt.Errorf("error reading size of %q: %v", filename, err) + return nil, fmt.Errorf("error reading size of %q: %w", filename, err) } blobSize = fileinfo.Size() blobTime = fileinfo.ModTime() @@ -168,10 +170,6 @@ func (r *tarballReference) NewImageSource(ctx context.Context, sys *types.System MediaType: blobTypes[i], }) } - annotations := make(map[string]string) - for k, v := range r.annotations { - annotations[k] = v - } manifest := imgspecv1.Manifest{ Versioned: imgspecs.Versioned{ SchemaVersion: 2, @@ -182,7 +180,7 @@ func (r *tarballReference) NewImageSource(ctx context.Context, sys *types.System MediaType: imgspecv1.MediaTypeImageConfig, }, Layers: layerDescriptors, - Annotations: annotations, + Annotations: maps.Clone(r.annotations), } // Encode the manifest. @@ -228,20 +226,19 @@ func (is *tarballImageSource) GetBlob(ctx context.Context, blobinfo types.BlobIn return io.NopCloser(bytes.NewBuffer(is.config)), is.configSize, nil } // Maybe one of the layer blobs. - for i := range is.blobIDs { - if blobinfo.Digest == is.blobIDs[i] { - // We want to read that layer: open the file or memory block and hand it back. - if is.filenames[i] == "-" { - return io.NopCloser(bytes.NewBuffer(is.reference.stdin)), int64(len(is.reference.stdin)), nil - } - reader, err := os.Open(is.filenames[i]) - if err != nil { - return nil, -1, fmt.Errorf("error opening %q: %v", is.filenames[i], err) - } - return reader, is.blobSizes[i], nil - } + i := slices.Index(is.blobIDs, blobinfo.Digest) + if i == -1 { + return nil, -1, fmt.Errorf("no blob with digest %q found", blobinfo.Digest.String()) + } + // We want to read that layer: open the file or memory block and hand it back. + if is.filenames[i] == "-" { + return io.NopCloser(bytes.NewBuffer(is.reference.stdin)), int64(len(is.reference.stdin)), nil + } + reader, err := os.Open(is.filenames[i]) + if err != nil { + return nil, -1, fmt.Errorf("error opening %q: %v", is.filenames[i], err) } - return nil, -1, fmt.Errorf("no blob with digest %q found", blobinfo.Digest.String()) + return reader, is.blobSizes[i], nil } // GetManifest returns the image's manifest along with its MIME type (which may be empty when it can't be determined but the manifest is available). diff --git a/transports/alltransports/alltransports.go b/transports/alltransports/alltransports.go index 62d767b583..1d9c2dc35d 100644 --- a/transports/alltransports/alltransports.go +++ b/transports/alltransports/alltransports.go @@ -25,24 +25,24 @@ import ( // ParseImageName converts a URL-like image name to a types.ImageReference. func ParseImageName(imgName string) (types.ImageReference, error) { // Keep this in sync with TransportFromImageName! - parts := strings.SplitN(imgName, ":", 2) - if len(parts) != 2 { + transportName, withinTransport, valid := strings.Cut(imgName, ":") + if !valid { return nil, fmt.Errorf(`Invalid image name "%s", expected colon-separated transport:reference`, imgName) } - transport := transports.Get(parts[0]) + transport := transports.Get(transportName) if transport == nil { - return nil, fmt.Errorf(`Invalid image name "%s", unknown transport "%s"`, imgName, parts[0]) + return nil, fmt.Errorf(`Invalid image name "%s", unknown transport "%s"`, imgName, transportName) } - return transport.ParseReference(parts[1]) + return transport.ParseReference(withinTransport) } // TransportFromImageName converts an URL-like name to a types.ImageTransport or nil when // the transport is unknown or when the input is invalid. func TransportFromImageName(imageName string) types.ImageTransport { // Keep this in sync with ParseImageName! - parts := strings.SplitN(imageName, ":", 2) - if len(parts) == 2 { - return transports.Get(parts[0]) + transportName, _, valid := strings.Cut(imageName, ":") + if valid { + return transports.Get(transportName) } return nil } diff --git a/transports/transports.go b/transports/transports.go index 46ee3710fc..834f33b489 100644 --- a/transports/transports.go +++ b/transports/transports.go @@ -5,6 +5,7 @@ import ( "sort" "sync" + "github.com/containers/image/v5/internal/set" "github.com/containers/image/v5/types" ) @@ -66,22 +67,21 @@ func Register(t types.ImageTransport) { // This is the generally recommended way to refer to images in the UI. // // NOTE: The returned string is not promised to be equal to the original input to ParseImageName; -// e.g. default attribute values omitted by the user may be filled in in the return value, or vice versa. +// e.g. default attribute values omitted by the user may be filled in the return value, or vice versa. func ImageName(ref types.ImageReference) string { return ref.Transport().Name() + ":" + ref.StringWithinTransport() } +var deprecatedTransports = set.NewWithValues("atomic") + // ListNames returns a list of non deprecated transport names. // Deprecated transports can be used, but are not presented to users. func ListNames() []string { kt.mu.Lock() defer kt.mu.Unlock() - deprecated := map[string]bool{ - "atomic": true, - } var names []string for _, transport := range kt.transports { - if !deprecated[transport.Name()] { + if !deprecatedTransports.Contains(transport.Name()) { names = append(names, transport.Name()) } } diff --git a/types/types.go b/types/types.go index 7075b09e09..6ea414b867 100644 --- a/types/types.go +++ b/types/types.go @@ -11,7 +11,7 @@ import ( v1 "github.com/opencontainers/image-spec/specs-go/v1" ) -// ImageTransport is a top-level namespace for ways to to store/load an image. +// ImageTransport is a top-level namespace for ways to store/load an image. // It should generally correspond to ImageSource/ImageDestination implementations. // // Note that ImageTransport is based on "ways the users refer to image storage", not necessarily on the underlying physical transport. @@ -48,7 +48,7 @@ type ImageReference interface { // StringWithinTransport returns a string representation of the reference, which MUST be such that // reference.Transport().ParseReference(reference.StringWithinTransport()) returns an equivalent reference. // NOTE: The returned string is not promised to be equal to the original input to ParseReference; - // e.g. default attribute values omitted by the user may be filled in in the return value, or vice versa. + // e.g. default attribute values omitted by the user may be filled in the return value, or vice versa. // WARNING: Do not use the return value in the UI to describe an image, it does not contain the Transport().Name() prefix; // instead, see transports.ImageName(). StringWithinTransport() string @@ -125,13 +125,20 @@ type BlobInfo struct { URLs []string Annotations map[string]string MediaType string + + // NOTE: The following fields contain desired _edits_ to blob infos. + // Conceptually then don't belong in the BlobInfo object at all; + // the edits should be provided specifically as parameters to the edit implementation. + // We can’t remove the fields without breaking compatibility, but don’t + // add any more. + // CompressionOperation is used in Image.UpdateLayerInfos to instruct // whether the original layer's "compressed or not" should be preserved, // possibly while changing the compression algorithm from one to another, // or if it should be compressed or decompressed. The field defaults to // preserve the original layer's compressedness. // TODO: To remove together with CryptoOperation in re-design to remove - // field out out of BlobInfo. + // field out of BlobInfo. CompressionOperation LayerCompression // CompressionAlgorithm is used in Image.UpdateLayerInfos to set the correct // MIME type for compressed layers (e.g., gzip or zstd). This field MUST be @@ -142,8 +149,9 @@ type BlobInfo struct { // CryptoOperation is used in Image.UpdateLayerInfos to instruct // whether the original layer was encrypted/decrypted // TODO: To remove together with CompressionOperation in re-design to - // remove field out out of BlobInfo. + // remove field out of BlobInfo. CryptoOperation LayerCrypto + // Before adding any fields to this struct, read the NOTE above. } // BICTransportScope encapsulates transport-dependent representation of a “scope” where blobs are or are not present. diff --git a/version/version.go b/version/version.go index ca5f4a42dd..7117f02d51 100644 --- a/version/version.go +++ b/version/version.go @@ -6,9 +6,9 @@ const ( // VersionMajor is for an API incompatible changes VersionMajor = 5 // VersionMinor is for functionality in a backwards-compatible manner - VersionMinor = 22 + VersionMinor = 24 // VersionPatch is for backwards-compatible bug fixes - VersionPatch = 1 + VersionPatch = 2 // VersionDev indicates development branch. Releases will be empty string. VersionDev = "-dev"