From f5729f6ac70261fbbadaac9151a0f88449615261 Mon Sep 17 00:00:00 2001 From: Radoslav Dimitrov Date: Sat, 21 Feb 2026 15:38:31 +0200 Subject: [PATCH 1/2] Move /verifier package Signed-off-by: Radoslav Dimitrov --- go.mod | 8 +- go.sum | 4 +- pkg/container/verifier/attestations.go | 103 ------ pkg/container/verifier/sigstore.go | 310 ------------------ .../tufroots/tuf-repo.github.com/root.json | 163 --------- pkg/container/verifier/utils.go | 130 -------- pkg/container/verifier/verifier.go | 262 --------------- pkg/runner/retriever/retriever.go | 4 +- 8 files changed, 8 insertions(+), 976 deletions(-) delete mode 100644 pkg/container/verifier/attestations.go delete mode 100644 pkg/container/verifier/sigstore.go delete mode 100644 pkg/container/verifier/tufroots/tuf-repo.github.com/root.json delete mode 100644 pkg/container/verifier/utils.go delete mode 100644 pkg/container/verifier/verifier.go diff --git a/go.mod b/go.mod index 039d51584b..77f662c469 100644 --- a/go.mod +++ b/go.mod @@ -41,10 +41,9 @@ require ( github.com/pressly/goose/v3 v3.26.0 github.com/prometheus/client_golang v1.23.2 github.com/redis/go-redis/v9 v9.18.0 - github.com/sigstore/protobuf-specs v0.5.0 - github.com/sigstore/sigstore-go v1.1.4 github.com/spf13/viper v1.21.0 - github.com/stacklok/toolhive-core v0.0.6 + github.com/stacklok/toolhive-catalog v0.0.0-20260220124241-3f98ae192446 + github.com/stacklok/toolhive-core v0.0.7 github.com/stretchr/testify v1.11.1 github.com/swaggo/swag/v2 v2.0.0-rc5 github.com/tailscale/hujson v0.0.0-20250605163823-992244df8c5a @@ -240,16 +239,17 @@ require ( github.com/sethvargo/go-retry v0.3.0 // indirect github.com/shibumi/go-pathspec v1.3.0 // indirect github.com/shirou/gopsutil/v4 v4.25.6 // indirect + github.com/sigstore/protobuf-specs v0.5.0 // indirect github.com/sigstore/rekor v1.5.0 // indirect github.com/sigstore/rekor-tiles/v2 v2.0.1 // indirect github.com/sigstore/sigstore v1.10.4 // indirect + github.com/sigstore/sigstore-go v1.1.4 // indirect github.com/sigstore/timestamp-authority/v2 v2.0.3 // indirect github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af // indirect github.com/skeema/knownhosts v1.3.1 // indirect github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect github.com/spf13/afero v1.15.0 // indirect github.com/spf13/cast v1.10.0 // indirect - github.com/stacklok/toolhive-catalog v0.0.0-20260220124241-3f98ae192446 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/sv-tools/openapi v0.4.0 // indirect diff --git a/go.sum b/go.sum index 222424ce16..29ebb5ec75 100644 --- a/go.sum +++ b/go.sum @@ -800,8 +800,8 @@ github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= github.com/stacklok/toolhive-catalog v0.0.0-20260220124241-3f98ae192446 h1:dRQhDz/3C9VUI9x5/8ofvLJhW+iJuS2JJEt72lCHw44= github.com/stacklok/toolhive-catalog v0.0.0-20260220124241-3f98ae192446/go.mod h1:mdpmpFak09Idzp1v5zTwjL9caj1pEdHlkPppFa2odPw= -github.com/stacklok/toolhive-core v0.0.6 h1:JLJpL4qyGh3z/fZKk+NNavziNCdtJlHoqroqBdWH6x8= -github.com/stacklok/toolhive-core v0.0.6/go.mod h1:7NPhRmr9TCuSZXT0kbsUDRDlcRhtLzi73ah9m2crvyI= +github.com/stacklok/toolhive-core v0.0.7 h1:AW0dmSDOJjUCynlUQMXH+ODNeqPWRHf370gkvRdMO+E= +github.com/stacklok/toolhive-core v0.0.7/go.mod h1:LhtXDsHbj5CcNKBJDzPAA2Y4fmYAEVDFSGlGuLkAsK4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= diff --git a/pkg/container/verifier/attestations.go b/pkg/container/verifier/attestations.go deleted file mode 100644 index 2eb4d9f933..0000000000 --- a/pkg/container/verifier/attestations.go +++ /dev/null @@ -1,103 +0,0 @@ -// SPDX-FileCopyrightText: Copyright 2025 Stacklok, Inc. -// SPDX-License-Identifier: Apache-2.0 - -package verifier - -import ( - "encoding/hex" - "fmt" - "io" - "log/slog" - "strings" - - "github.com/google/go-containerregistry/pkg/authn" - "github.com/google/go-containerregistry/pkg/name" - "github.com/google/go-containerregistry/pkg/v1/remote" - containerdigest "github.com/opencontainers/go-digest" - "github.com/sigstore/sigstore-go/pkg/bundle" -) - -// bundleFromAttestation retrieves the attestation bundles from the image reference. Note that the attestation -// bundles are stored as OCI image references. The function uses the referrers API to get the attestation. GitHub supports -// discovering the attestations via their API, but this is not supported here for now. -func bundleFromAttestation(imageRef string, keychain authn.Keychain) ([]sigstoreBundle, error) { - var bundles []sigstoreBundle - - // Get the auth options - opts := []remote.Option{remote.WithAuthFromKeychain(keychain)} - - // Get the image reference - ref, err := name.ParseReference(imageRef) - if err != nil { - return nil, fmt.Errorf("error parsing image reference: %w", err) - } - - // Get the image descriptor - desc, err := remote.Get(ref, opts...) - if err != nil { - return nil, fmt.Errorf("error getting image descriptor: %w", err) - } - - // Get the digest - digest := ref.Context().Digest(desc.Digest.String()) - - // Get the digest in bytes - digestByte, err := hex.DecodeString(desc.Digest.Hex) - if err != nil { - return nil, err - } - - // Use the referrers API to get the attestation reference - referrers, err := remote.Referrers(digest, opts...) - if err != nil { - return nil, fmt.Errorf("error getting referrers: %w, %s", ErrProvenanceNotFoundOrIncomplete, err.Error()) - } - - refManifest, err := referrers.IndexManifest() - if err != nil { - return nil, fmt.Errorf("error getting referrers manifest: %w, %s", ErrProvenanceNotFoundOrIncomplete, err.Error()) - } - - // Loop through all available attestations and extract the bundle - for _, refDesc := range refManifest.Manifests { - if !strings.HasPrefix(refDesc.ArtifactType, "application/vnd.dev.sigstore.bundle") { - continue - } - refImg, err := remote.Image(ref.Context().Digest(refDesc.Digest.String()), opts...) - if err != nil { - slog.Debug("error getting referrer image", "error", err) - continue - } - layers, err := refImg.Layers() - if err != nil { - slog.Debug("error getting referrer layers", "error", err) - continue - } - layer0, err := layers[0].Uncompressed() - if err != nil { - slog.Debug("error uncompressing referrer layer", "error", err) - continue - } - bundleBytes, err := io.ReadAll(layer0) - if err != nil { - slog.Debug("error reading referrer layer", "error", err) - continue - } - b := &bundle.Bundle{} - err = b.UnmarshalJSON(bundleBytes) - if err != nil { - slog.Debug("error unmarshalling bundle", "error", err) - continue - } - - bundles = append(bundles, sigstoreBundle{ - bundle: b, - digestBytes: digestByte, - digestAlgo: containerdigest.Canonical.String(), - }) - } - if len(bundles) == 0 { - return nil, ErrProvenanceNotFoundOrIncomplete - } - return bundles, nil -} diff --git a/pkg/container/verifier/sigstore.go b/pkg/container/verifier/sigstore.go deleted file mode 100644 index 151f253fea..0000000000 --- a/pkg/container/verifier/sigstore.go +++ /dev/null @@ -1,310 +0,0 @@ -// SPDX-FileCopyrightText: Copyright 2025 Stacklok, Inc. -// SPDX-License-Identifier: Apache-2.0 - -// Package verifier provides a client for verifying artifacts using sigstore -package verifier - -import ( - "bytes" - "encoding/base64" - "encoding/hex" - "encoding/json" - "encoding/pem" - "errors" - "fmt" - "io" - "log/slog" - - "github.com/google/go-containerregistry/pkg/authn" - "github.com/google/go-containerregistry/pkg/crane" - "github.com/google/go-containerregistry/pkg/name" - v1 "github.com/google/go-containerregistry/pkg/v1" - "github.com/google/go-containerregistry/pkg/v1/remote" - protobundle "github.com/sigstore/protobuf-specs/gen/pb-go/bundle/v1" - protocommon "github.com/sigstore/protobuf-specs/gen/pb-go/common/v1" - protorekor "github.com/sigstore/protobuf-specs/gen/pb-go/rekor/v1" - "github.com/sigstore/sigstore-go/pkg/bundle" -) - -type sigstoreBundle struct { - bundle *bundle.Bundle - digestBytes []byte - digestAlgo string -} - -// bundleFromSigstoreSignedImage returns a bundle from a Sigstore signed image -func bundleFromSigstoreSignedImage(imageRef string, keychain authn.Keychain) ([]sigstoreBundle, error) { - // Get the signature manifest from the OCI image reference - signatureRef, err := getSignatureReferenceFromOCIImage(imageRef, keychain) - if err != nil { - return nil, fmt.Errorf("error getting signature reference from OCI image: %w", err) - } - - // Parse the manifest and return a list of all simple signing layers we managed to extract - simpleSigningLayers, err := getSimpleSigningLayersFromSignatureManifest(signatureRef, keychain) - if err != nil { - return nil, fmt.Errorf("%w: %s", ErrProvenanceNotFoundOrIncomplete, err.Error()) - } - - // Loop through each and build the sigstore bundles - var bundles []sigstoreBundle - for _, layer := range simpleSigningLayers { - // Build the verification material for the bundle - verificationMaterial, err := getBundleVerificationMaterial(layer) - if err != nil { - slog.Error("error getting bundle verification material") - continue - } - - // Build the message signature for the bundle - msgSignature, err := getBundleMsgSignature(layer) - if err != nil { - slog.Error("error getting bundle message signature") - continue - } - - // Construct and verify the bundle - pbb := protobundle.Bundle{ - MediaType: sigstoreBundleMediaType01, - VerificationMaterial: verificationMaterial, - Content: msgSignature, - } - bun, err := bundle.NewBundle(&pbb) - if err != nil { - slog.Error("error creating protobuf bundle") - continue - } - - // Collect the digest of the simple signing layer (this is what is signed) - digestBytes, err := hex.DecodeString(layer.Digest.Hex) - if err != nil { - slog.Error("error decoding the simplesigning layer digest") - continue - } - - // Store the bundle and the certificate identity we extracted from the simple signing layer - bundles = append(bundles, sigstoreBundle{ - bundle: bun, - digestAlgo: layer.Digest.Algorithm, - digestBytes: digestBytes, - }) - } - - // There's no available provenance information about this image if we failed to find valid bundles from the list - // of simple signing layers - if len(bundles) == 0 { - return nil, ErrProvenanceNotFoundOrIncomplete - } - - // Return the bundles - return bundles, nil -} - -// getSignatureReferenceFromOCIImage returns the simple signing layer from the OCI image reference -func getSignatureReferenceFromOCIImage(imageRef string, keychain authn.Keychain) (string, error) { - // 0. Get the auth options - opts := []remote.Option{remote.WithAuthFromKeychain(keychain)} - - // 1. Get the image reference - ref, err := name.ParseReference(imageRef) - if err != nil { - return "", fmt.Errorf("error parsing image reference: %w", err) - } - - // 2. Get the image descriptor - desc, err := remote.Get(ref, opts...) - if err != nil { - return "", fmt.Errorf("error getting image descriptor: %w", err) - } - - // 3. Get the digest - digest := ref.Context().Digest(desc.Digest.String()) - h, err := v1.NewHash(digest.Identifier()) - if err != nil { - return "", fmt.Errorf("error getting hash: %w", err) - } - - // 4. Construct the signature reference - sha256-.sig - sigTag := digest.Context().Tag(fmt.Sprint(h.Algorithm, "-", h.Hex, ".sig")) - - // 5. Return the reference - return sigTag.Name(), nil -} - -// getSimpleSigningLayersFromSignatureManifest returns the identity and issuer from the certificate -func getSimpleSigningLayersFromSignatureManifest(manifestRef string, keychain authn.Keychain) ([]v1.Descriptor, error) { - craneOpts := []crane.Option{crane.WithAuthFromKeychain(keychain)} - // Get the manifest of the signature - mf, err := crane.Manifest(manifestRef, craneOpts...) - if err != nil { - return nil, fmt.Errorf("error getting signature manifest: %w", err) - } - - // Parse the manifest - r := io.LimitReader(bytes.NewReader(mf), MaxAttestationsBytesLimit) - manifest, err := v1.ParseManifest(r) - if err != nil { - return nil, fmt.Errorf("error parsing signature manifest: %w", err) - } - - // Loop through its layers and extract the simple signing layers - var results []v1.Descriptor - for _, layer := range manifest.Layers { - if layer.MediaType == "application/vnd.dev.cosign.simplesigning.v1+json" { - // We found a simple signing layer, store and return it even if we may fail to parse it later - results = append(results, layer) - } - } - - // Return the results - we may not have found any simple signing layers, but we still return the results - return results, nil -} - -// getBundleVerificationMaterial returns the bundle verification material from the simple signing layer -func getBundleVerificationMaterial(manifestLayer v1.Descriptor) ( - *protobundle.VerificationMaterial, error) { - // 1. Get the signing certificate chain - signingCert, err := getVerificationMaterialX509CertificateChain(manifestLayer) - if err != nil { - return nil, fmt.Errorf("error getting signing certificate: %w", err) - } - - // 2. Get the transparency log entries - tlogEntries, err := getVerificationMaterialTlogEntries(manifestLayer) - if err != nil { - return nil, fmt.Errorf("error getting tlog entries: %w", err) - } - // 3. Construct the verification material - return &protobundle.VerificationMaterial{ - Content: signingCert, - TlogEntries: tlogEntries, - TimestampVerificationData: nil, - }, nil -} - -// getVerificationMaterialX509CertificateChain returns the verification material X509 certificate chain from the -// simple signing layer -func getVerificationMaterialX509CertificateChain(manifestLayer v1.Descriptor) ( - *protobundle.VerificationMaterial_X509CertificateChain, error) { - // 1. Get the PEM certificate from the simple signing layer - pemCert := manifestLayer.Annotations["dev.sigstore.cosign/certificate"] - // 2. Construct the DER encoded version of the PEM certificate - block, _ := pem.Decode([]byte(pemCert)) - if block == nil { - return nil, errors.New("failed to decode PEM block") - } - signingCert := protocommon.X509Certificate{ - RawBytes: block.Bytes, - } - // 3. Construct the X509 certificate chain - return &protobundle.VerificationMaterial_X509CertificateChain{ - X509CertificateChain: &protocommon.X509CertificateChain{ - Certificates: []*protocommon.X509Certificate{&signingCert}, - }, - }, nil -} - -// getVerificationMaterialTlogEntries returns the verification material transparency log entries from the simple signing layer -func getVerificationMaterialTlogEntries(manifestLayer v1.Descriptor) ( - []*protorekor.TransparencyLogEntry, error) { - // 1. Get the bundle annotation - bun := manifestLayer.Annotations["dev.sigstore.cosign/bundle"] - var jsonData map[string]interface{} - err := json.Unmarshal([]byte(bun), &jsonData) - if err != nil { - return nil, fmt.Errorf("error unmarshaling json: %w", err) - } - // 2. Get the log index, log ID, integrated time, signed entry timestamp and body - logIndex, ok := jsonData["Payload"].(map[string]interface{})["logIndex"].(float64) - if !ok { - return nil, fmt.Errorf("error getting logIndex") - } - logIndexInt64 := int64(logIndex) - li, ok := jsonData["Payload"].(map[string]interface{})["logID"].(string) - if !ok { - return nil, fmt.Errorf("error getting logID") - } - logID, err := hex.DecodeString(li) - if err != nil { - return nil, fmt.Errorf("error decoding logID: %w", err) - } - integratedTime, ok := jsonData["Payload"].(map[string]interface{})["integratedTime"].(float64) - if !ok { - return nil, fmt.Errorf("error getting integratedTime") - } - set, ok := jsonData["SignedEntryTimestamp"].(string) - if !ok { - return nil, fmt.Errorf("error getting SignedEntryTimestamp") - } - signedEntryTimestamp, err := base64.StdEncoding.DecodeString(set) - if err != nil { - return nil, fmt.Errorf("error decoding signedEntryTimestamp: %w", err) - } - // 3. Unmarshal the body and extract the rekor KindVersion details - body, ok := jsonData["Payload"].(map[string]interface{})["body"].(string) - if !ok { - return nil, fmt.Errorf("error getting body") - } - bodyBytes, err := base64.StdEncoding.DecodeString(body) - if err != nil { - return nil, fmt.Errorf("error decoding body: %w", err) - } - err = json.Unmarshal(bodyBytes, &jsonData) - if err != nil { - return nil, fmt.Errorf("error unmarshaling json: %w", err) - } - apiVersion := jsonData["apiVersion"].(string) - kind := jsonData["kind"].(string) - // 4. Construct the transparency log entry list - return []*protorekor.TransparencyLogEntry{ - { - LogIndex: logIndexInt64, - LogId: &protocommon.LogId{ - KeyId: logID, - }, - KindVersion: &protorekor.KindVersion{ - Kind: kind, - Version: apiVersion, - }, - IntegratedTime: int64(integratedTime), - InclusionPromise: &protorekor.InclusionPromise{ - SignedEntryTimestamp: signedEntryTimestamp, - }, - InclusionProof: nil, - CanonicalizedBody: bodyBytes, - }, - }, nil -} - -// getBundleMsgSignature returns the bundle message signature from the simple signing layer -func getBundleMsgSignature(simpleSigningLayer v1.Descriptor) (*protobundle.Bundle_MessageSignature, error) { - // 1. Get the message digest algorithm - var msgHashAlg protocommon.HashAlgorithm - switch simpleSigningLayer.Digest.Algorithm { - case "sha256": - msgHashAlg = protocommon.HashAlgorithm_SHA2_256 - default: - return nil, fmt.Errorf("unknown digest algorithm: %s", simpleSigningLayer.Digest.Algorithm) - } - // 2. Get the message digest - digest, err := hex.DecodeString(simpleSigningLayer.Digest.Hex) - if err != nil { - return nil, fmt.Errorf("error decoding digest: %w", err) - } - // 3. Get the signature - s := simpleSigningLayer.Annotations["dev.cosignproject.cosign/signature"] - sig, err := base64.StdEncoding.DecodeString(s) - if err != nil { - return nil, fmt.Errorf("error decoding manSig: %w", err) - } - // Construct the bundle message signature - return &protobundle.Bundle_MessageSignature{ - MessageSignature: &protocommon.MessageSignature{ - MessageDigest: &protocommon.HashOutput{ - Algorithm: msgHashAlg, - Digest: digest, - }, - Signature: sig, - }, - }, nil -} diff --git a/pkg/container/verifier/tufroots/tuf-repo.github.com/root.json b/pkg/container/verifier/tufroots/tuf-repo.github.com/root.json deleted file mode 100644 index 3b6b0734b5..0000000000 --- a/pkg/container/verifier/tufroots/tuf-repo.github.com/root.json +++ /dev/null @@ -1,163 +0,0 @@ -{ - "signatures": [ - { - "keyid": "a10513a5ab61acd0c6b6fbe0504856ead18f3b17c4fabbe3fa848c79a5a187cf", - "sig": "304402203c8f5f7443f7052923e82f9ca0b1bb61a33498444076a2f43e1285a47f6e562d022014de99a7e5413440896b6804944e3c49390cfe6e617211b8dc42a8e67675bc13" - }, - { - "keyid": "d6a89e23fb22801a0d1186bf1bdd007e228f65a8aa9964d24d06cb5fbb0ce91c", - "sig": "3044022009a20307f974af7e05cc9564eea497f45062e3b21272d1062713b3d22c868298022059d032ad973a28bdbd03959cf96b21398b6b6e2ca618c17ce6c13712246343a2" - }, - { - "keyid": "539dde44014c850fe6eeb8b299eb7dae2e1f4bf83454b949e98aa73542cdc65a", - "sig": "3045022100edd270d36d0c8468b9a1f2ef1c81a270c72ffd50c65bca0ed1ebd424df09f64b022002b27ffafd7bc5bdfc25281b5b9b597cf2d67d4eeb4af2ff45eb3e666b860c21" - }, - { - "keyid": "88737ccdac7b49cc237e9aaead81be2a40278b886a693d8149a19cf543f093d3", - "sig": "30460221008d7d95434e576d5876b2db30fd645505ca546618bbc7a8e4b39f64e6a36df9ad022100c00a5294e4ddd02d48d28918b87a06bdfdeccd0618ecdcec29bb2597a05fe474" - }, - { - "keyid": "5e01c9a0b2641a8965a4a74e7df0bc7b2d8278a2c3ca0cf7a3f2f783d3c69800", - "sig": "30450220215fb3d19d94560a3a2a6067a71c92daf867d13700c9500c4c32d8009a48a634022100df9fb6cee786313bf6c363daac4de39b3dd531f211f81d2391c41bd2d0f91a80" - }, - { - "keyid": "4f4d1dd75f2d7f3860e3a068d7bed90dec5f0faafcbe1ace7fb7d95d29e07228", - "sig": "304502204091ac5e61b6462d262ecc8442781dd09843bed39942a95a4884c8c6a2c212ef022100dcee86392748f48950d04d539ac1a6643ed1f0b4bd6856f8aeb5a135826c846f" - }, - { - "keyid": "eb8eff37f93af2faaba519f341decec3cecd3eeafcace32966db9723842c8a62", - "sig": "30460221009188548601a43b501223caeefca4876ae892e18a85c885004b4c8aeeb05a4421022100abdcc72d94597f8297d6297897ff96f285168dbe6b3d57f846dbc7a2948d2935" - }, - { - "keyid": "8b498a80a1b7af188c10c9abdf6aade81d14faaffcde2abcd6063baa673ebd12", - "sig": "3046022100b440561545d48759dc4140cda9f8af7c9405a101d6136dd0a26edd6562b7064f022100cafa917ed90350494e47d226b64a8ec63ef5ceebb8ba4d2dec2ce018e4ad402a" - } - ], - "signed": { - "_type": "root", - "consistent_snapshot": true, - "expires": "2024-06-23T08:29:18Z", - "keys": { - "4f4d1dd75f2d7f3860e3a068d7bed90dec5f0faafcbe1ace7fb7d95d29e07228": { - "keytype": "ecdsa", - "keyval": { - "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAENki7aZVips5SgRzCd/Om0CGzQKY/\nnv84giqVDmdwb2ys82Z6soFLasvYYEEQcwqaC170n9gr93wHUgPc796uJA==\n-----END PUBLIC KEY-----\n" - }, - "scheme": "ecdsa-sha2-nistp256", - "x-tuf-on-ci-keyowner": "@ashtom" - }, - "539dde44014c850fe6eeb8b299eb7dae2e1f4bf83454b949e98aa73542cdc65a": { - "keytype": "ecdsa", - "keyval": { - "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAElD0o2sOZN9n3RKQ7PtMLAoXj+2Ai\nn4PKT/pfnzDlNLrD3VTQwCc4sR4t+OLu4KQ+qk+kXkR9YuBsu3bdJZ1OWw==\n-----END PUBLIC KEY-----\n" - }, - "scheme": "ecdsa-sha2-nistp256", - "x-tuf-on-ci-keyowner": "@nerdneha" - }, - "5e01c9a0b2641a8965a4a74e7df0bc7b2d8278a2c3ca0cf7a3f2f783d3c69800": { - "keytype": "ecdsa", - "keyval": { - "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEC9RNAsuDCNO6T7qA7Y5F8orw2tIW\nr7rUr4ffxvzTMrbkVtjR/trtE0q0+T0zQ8TWLyI6EYMwb947ej2ItfkOyA==\n-----END PUBLIC KEY-----\n" - }, - "scheme": "ecdsa-sha2-nistp256", - "x-tuf-on-ci-keyowner": "@jacobdepriest" - }, - "88737ccdac7b49cc237e9aaead81be2a40278b886a693d8149a19cf543f093d3": { - "keytype": "ecdsa", - "keyval": { - "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEBagkskNOpOTbetTX5CdnvMy+LiWn\nonRrNrqAHL4WgiebH7Uig7GLhC3bkeA/qgb926/vr9qhOPG9Buj2HatrPw==\n-----END PUBLIC KEY-----\n" - }, - "scheme": "ecdsa-sha2-nistp256", - "x-tuf-on-ci-keyowner": "@gregose" - }, - "8b498a80a1b7af188c10c9abdf6aade81d14faaffcde2abcd6063baa673ebd12": { - "keytype": "ecdsa", - "keyval": { - "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE7IEoVNwrprchXGhT5sAhSax7SOd3\n8duuISghCzfmHdKJWSbV2wJRamRiUVRtmA83K/qm5cT20WXMCT5QeM/D3A==\n-----END PUBLIC KEY-----\n" - }, - "scheme": "ecdsa-sha2-nistp256", - "x-tuf-on-ci-keyowner": "@trevrosen" - }, - "a10513a5ab61acd0c6b6fbe0504856ead18f3b17c4fabbe3fa848c79a5a187cf": { - "keytype": "ecdsa", - "keyval": { - "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEC2wJ3xscyXxBLybJ9FVjwkyQMe53\nRHUz77AjMO8MzVaT8xw6ZvJqdNZiytYtigWULlINxw6frNsWJKb/f7lC8A==\n-----END PUBLIC KEY-----\n" - }, - "scheme": "ecdsa-sha2-nistp256", - "x-tuf-on-ci-keyowner": "@kommendorkapten" - }, - "d6a89e23fb22801a0d1186bf1bdd007e228f65a8aa9964d24d06cb5fbb0ce91c": { - "keytype": "ecdsa", - "keyval": { - "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEDdORwcruW3gqAgaLjH/nNdGMB4kQ\nAvA+wD6DyO4P/wR8ee2ce83NZHq1ZADKhve0rlYKaKy3CqyQ5SmlZ36Zhw==\n-----END PUBLIC KEY-----\n" - }, - "scheme": "ecdsa-sha2-nistp256", - "x-tuf-on-ci-keyowner": "@krukow" - }, - "eb8eff37f93af2faaba519f341decec3cecd3eeafcace32966db9723842c8a62": { - "keytype": "ecdsa", - "keyval": { - "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAENynVdQnM9h7xU71G7PiJpQaDemub\nkbjsjYwLlPJTQVuxQO8WeIpJf8MEh5rf01t2dDIuCsZ5gRx+QvDv0UzfsA==\n-----END PUBLIC KEY-----\n" - }, - "scheme": "ecdsa-sha2-nistp256", - "x-tuf-on-ci-keyowner": "@mph4" - }, - "eb9799b483affac9da87ef4c9ea467928415c961349e607e5e6e485679b07f8f": { - "keytype": "ecdsa", - "keyval": { - "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAENKNcNcX+d73lS1TRFb9Vnp8JvOoh\nzYQ+in43iGenbG8RGo9L/6FJ2hoRbVU6xskvyuErcdPbCdI4GxrQ5i8hkw==\n-----END PUBLIC KEY-----\n" - }, - "scheme": "ecdsa-sha2-nistp256", - "x-tuf-on-ci-online-uri": "azurekms://production-tuf-root.vault.azure.net/keys/Online-Key/aaf375fd8ed24acb949a5cc173700b05" - } - }, - "roles": { - "root": { - "keyids": [ - "a10513a5ab61acd0c6b6fbe0504856ead18f3b17c4fabbe3fa848c79a5a187cf", - "4f4d1dd75f2d7f3860e3a068d7bed90dec5f0faafcbe1ace7fb7d95d29e07228", - "88737ccdac7b49cc237e9aaead81be2a40278b886a693d8149a19cf543f093d3", - "5e01c9a0b2641a8965a4a74e7df0bc7b2d8278a2c3ca0cf7a3f2f783d3c69800", - "d6a89e23fb22801a0d1186bf1bdd007e228f65a8aa9964d24d06cb5fbb0ce91c", - "eb8eff37f93af2faaba519f341decec3cecd3eeafcace32966db9723842c8a62", - "8b498a80a1b7af188c10c9abdf6aade81d14faaffcde2abcd6063baa673ebd12", - "539dde44014c850fe6eeb8b299eb7dae2e1f4bf83454b949e98aa73542cdc65a" - ], - "threshold": 3 - }, - "snapshot": { - "keyids": [ - "eb9799b483affac9da87ef4c9ea467928415c961349e607e5e6e485679b07f8f" - ], - "threshold": 1, - "x-tuf-on-ci-expiry-period": 21, - "x-tuf-on-ci-signing-period": 7 - }, - "targets": { - "keyids": [ - "a10513a5ab61acd0c6b6fbe0504856ead18f3b17c4fabbe3fa848c79a5a187cf", - "4f4d1dd75f2d7f3860e3a068d7bed90dec5f0faafcbe1ace7fb7d95d29e07228", - "88737ccdac7b49cc237e9aaead81be2a40278b886a693d8149a19cf543f093d3", - "5e01c9a0b2641a8965a4a74e7df0bc7b2d8278a2c3ca0cf7a3f2f783d3c69800", - "d6a89e23fb22801a0d1186bf1bdd007e228f65a8aa9964d24d06cb5fbb0ce91c", - "eb8eff37f93af2faaba519f341decec3cecd3eeafcace32966db9723842c8a62", - "8b498a80a1b7af188c10c9abdf6aade81d14faaffcde2abcd6063baa673ebd12", - "539dde44014c850fe6eeb8b299eb7dae2e1f4bf83454b949e98aa73542cdc65a" - ], - "threshold": 3 - }, - "timestamp": { - "keyids": [ - "eb9799b483affac9da87ef4c9ea467928415c961349e607e5e6e485679b07f8f" - ], - "threshold": 1, - "x-tuf-on-ci-expiry-period": 7, - "x-tuf-on-ci-signing-period": 6 - } - }, - "spec_version": "1.0.31", - "version": 1, - "x-tuf-on-ci-expiry-period": 240, - "x-tuf-on-ci-signing-period": 60 - } -} \ No newline at end of file diff --git a/pkg/container/verifier/utils.go b/pkg/container/verifier/utils.go deleted file mode 100644 index 7933e0947e..0000000000 --- a/pkg/container/verifier/utils.go +++ /dev/null @@ -1,130 +0,0 @@ -// SPDX-FileCopyrightText: Copyright 2025 Stacklok, Inc. -// SPDX-License-Identifier: Apache-2.0 - -package verifier - -import ( - "embed" - "errors" - "fmt" - "net/url" - "path" - - "github.com/google/go-containerregistry/pkg/authn" - "github.com/sigstore/sigstore-go/pkg/tuf" - "github.com/sigstore/sigstore-go/pkg/verify" -) - -//go:embed tufroots -var embeddedTufRoots embed.FS - -var ( - // ErrProvenanceNotFoundOrIncomplete is returned when there's no provenance info (missing .sig or attestation) or - // has incomplete data - ErrProvenanceNotFoundOrIncomplete = errors.New("provenance not found or incomplete") - - // ErrProvenanceServerInformationNotSet is returned when the provenance information for a server is not set - ErrProvenanceServerInformationNotSet = errors.New("provenance server information not set") - - // MaxAttestationsBytesLimit is the maximum number of bytes we're willing to read from the attestation endpoint - // We'll limit this to 10mb for now - MaxAttestationsBytesLimit int64 = 10 * 1024 * 1024 -) - -const ( - sigstoreBundleMediaType01 = "application/vnd.dev.sigstore.bundle+json;version=0.1" - // githubTokenIssuer is the issuer stamped into sigstore certs - // when authenticating through GitHub tokens - //nolint: gosec // Not an embedded credential - githubTokenIssuer = "https://token.actions.githubusercontent.com" -) - -func verifierOptions(trustedRoot string) ([]verify.VerifierOption, error) { - switch trustedRoot { - case TrustedRootSigstorePublicGoodInstance: - return []verify.VerifierOption{ - verify.WithSignedCertificateTimestamps(1), - verify.WithTransparencyLog(1), - verify.WithObserverTimestamps(1), - }, nil - case TrustedRootSigstoreGitHub: - return []verify.VerifierOption{ - verify.WithObserverTimestamps(1), - }, nil - } - return nil, fmt.Errorf("unknown trusted root: %s", trustedRoot) -} - -func getSigstoreOptions(sigstoreTUFRepoURL string) (*tuf.Options, []verify.VerifierOption, error) { - // Default the sigstoreTUFRepoURL to the sigstore public trusted root repo if not provided - if sigstoreTUFRepoURL == "" { - sigstoreTUFRepoURL = TrustedRootSigstorePublicGoodInstance - } - - // Get the Sigstore TUF client options - tufOpts, err := getTUFOptions(sigstoreTUFRepoURL) - if err != nil { - return nil, nil, err - } - - // Get the Sigstore verifier options - opts, err := verifierOptions(sigstoreTUFRepoURL) - if err != nil { - return nil, nil, err - } - - // All good - return tufOpts, opts, nil -} - -func getTUFOptions(sigstoreTUFRepoURL string) (*tuf.Options, error) { - // Default the TUF options - tufOpts := tuf.DefaultOptions() - tufOpts.DisableLocalCache = true - - // Set the repository base URL, fix the scheme if not provided - tufURL, err := url.Parse(sigstoreTUFRepoURL) - if err != nil { - return nil, fmt.Errorf("error parsing sigstore TUF repo URL: %w", err) - } - if tufURL.Scheme == "" { - tufURL.Scheme = "https" - } - tufOpts.RepositoryBaseURL = tufURL.String() - - // sigstore-go has a copy of the root.json for the public sigstore instance embedded. Nothing to do. - if sigstoreTUFRepoURL != TrustedRootSigstorePublicGoodInstance { - // Look up and set the embedded root.json for the given TUF repository - rootJson, err := embeddedRootJson(sigstoreTUFRepoURL) - if err != nil { - return nil, fmt.Errorf("error getting embedded root.json for %s: %w", sigstoreTUFRepoURL, err) - } - tufOpts.Root = rootJson - } - - // All good - return tufOpts, nil -} - -func embeddedRootJson(tufRootURL string) ([]byte, error) { - embeddedRootPath := path.Join("tufroots", tufRootURL, rootTUFPath) - return embeddedTufRoots.ReadFile(embeddedRootPath) -} - -// getSigstoreBundles returns the sigstore bundles, either through the OCI registry or the GitHub attestation endpoint -func getSigstoreBundles( - imageRef string, - keychain authn.Keychain, -) ([]sigstoreBundle, error) { - // Try to build a bundle from a Sigstore signed image - bundles, err := bundleFromSigstoreSignedImage(imageRef, keychain) - if errors.Is(err, ErrProvenanceNotFoundOrIncomplete) { - // If we get this error, it means that the image is not signed - // or the signature is incomplete. Let's try to see if we can find attestation for the image. - return bundleFromAttestation(imageRef, keychain) - } else if err != nil { - return nil, err - } - // If we get here, it means that we got a bundle from a Sigstore signed image - return bundles, nil -} diff --git a/pkg/container/verifier/verifier.go b/pkg/container/verifier/verifier.go deleted file mode 100644 index a7b5c449f6..0000000000 --- a/pkg/container/verifier/verifier.go +++ /dev/null @@ -1,262 +0,0 @@ -// SPDX-FileCopyrightText: Copyright 2025 Stacklok, Inc. -// SPDX-License-Identifier: Apache-2.0 - -package verifier - -import ( - "errors" - "fmt" - "log/slog" - "reflect" - "strings" - - "github.com/google/go-containerregistry/pkg/authn" - "github.com/sigstore/sigstore-go/pkg/fulcio/certificate" - "github.com/sigstore/sigstore-go/pkg/root" - "github.com/sigstore/sigstore-go/pkg/verify" - - "github.com/stacklok/toolhive-core/registry/types" - "github.com/stacklok/toolhive/pkg/container/images" -) - -const ( - // TrustedRootSigstoreGitHub is the GitHub trusted root repository for sigstore (used for private repos, Enterprise) - TrustedRootSigstoreGitHub = "tuf-repo.github.com" - // TrustedRootSigstorePublicGoodInstance is the public trusted root repository for sigstore - TrustedRootSigstorePublicGoodInstance = "tuf-repo-cdn.sigstore.dev" - // RootTUFPath is the path to the root.json file inside an embedded TUF repository - rootTUFPath = "root.json" -) - -// Sigstore is the sigstore verifier -type Sigstore struct { - verifier *verify.Verifier - keychain authn.Keychain -} - -// Result is the result of the verification -type Result struct { - IsSigned bool `json:"is_signed"` - IsVerified bool `json:"is_verified"` - verify.VerificationResult -} - -// New creates a new Sigstore verifier -func New(serverInfo *registry.ImageMetadata) (*Sigstore, error) { - // Fail the verification early if the server information is not set - if serverInfo == nil || serverInfo.Provenance == nil { - return nil, ErrProvenanceServerInformationNotSet - } - sigstoreTUFRepoURL := serverInfo.Provenance.SigstoreURL - - // Default the sigstoreTUFRepoURL to the sigstore public trusted root repo if not provided. - // Note: Update this if we want to support more sigstore instances - if sigstoreTUFRepoURL == "" { - sigstoreTUFRepoURL = TrustedRootSigstorePublicGoodInstance - } - - // Get the sigstore options for the TUF client and the verifier - tufOpts, opts, err := getSigstoreOptions(sigstoreTUFRepoURL) - if err != nil { - return nil, err - } - - // Get the trusted material - sigstore's trusted_root.json - trustedMaterial, err := root.FetchTrustedRootWithOptions(tufOpts) - if err != nil { - return nil, err - } - - sev, err := verify.NewVerifier(trustedMaterial, opts...) - if err != nil { - return nil, err - } - - // return the verifier - return &Sigstore{ - verifier: sev, - keychain: images.NewCompositeKeychain(), - }, nil -} - -// WithKeychain sets the keychain for authentication -func (s *Sigstore) WithKeychain(keychain authn.Keychain) *Sigstore { - s.keychain = keychain - return s -} - -// GetVerificationResults returns the verification results for the given image reference -func (s *Sigstore) GetVerificationResults( - imageRef string, -) ([]*verify.VerificationResult, error) { - // Construct the bundle(s) for the image reference - bundles, err := getSigstoreBundles(imageRef, s.keychain) - if err != nil && !errors.Is(err, ErrProvenanceNotFoundOrIncomplete) { - // We got some other unexpected error prior to querying for the signature/attestation - return nil, err - } - //nolint:gosec // G706: bundle count derived from external registry data - slog.Debug("sigstore bundles constructed", "count", len(bundles)) - - // If we didn't manage to construct any valid bundles, it probably means that the image is not signed. - if len(bundles) == 0 || errors.Is(err, ErrProvenanceNotFoundOrIncomplete) { - return []*verify.VerificationResult{}, nil - } - - // Construct the verification result for each bundle we managed to generate. - return getVerifiedResults(s.verifier, bundles), nil -} - -// getVerifiedResults verifies the artifact using the bundles against the configured sigstore instance -// and returns the extracted metadata that we need for ingestion -func getVerifiedResults( - sev *verify.Verifier, - bundles []sigstoreBundle, -) []*verify.VerificationResult { - var results []*verify.VerificationResult - - // Verify each bundle we've constructed - for _, b := range bundles { - // Create a new verification result. At this point, we managed to extract a bundle, so lets verify it. - verificationResult, err := sev.Verify(b.bundle, verify.NewPolicy( - verify.WithArtifactDigest(b.digestAlgo, b.digestBytes), - verify.WithoutIdentitiesUnsafe(), - )) - if err != nil { - slog.Info("bundle verification failed", "error", err) - continue - } - // We've successfully verified and extracted the artifact provenance information - results = append(results, verificationResult) - } - // Return the results - return results -} - -// VerifyServer verifies the server information for the given image reference -func (s *Sigstore) VerifyServer(imageRef string, serverInfo *registry.ImageMetadata) (bool, error) { - // Get the verification results for the image reference - results, err := s.GetVerificationResults(imageRef) - if err != nil { - return false, err - } - - // If we didn't manage to get any verification results, it probably means that the image is not signed. - if len(results) == 0 { - return false, nil - } - - // Compare the server information with the verification results - for _, res := range results { - if !isVerificationResultMatchingServerProvenance(res, serverInfo.Provenance) { - // The server information does not match the verification result, fail the verification - return false, nil - } - } - // The server information matches the verification result, pass the verification - return true, nil -} - -func isVerificationResultMatchingServerProvenance(r *verify.VerificationResult, p *registry.Provenance) bool { - if r == nil || p == nil || r.Signature == nil || r.Signature.Certificate == nil { - return false - } - - // Compare the base properties of the verification result and the server provenance - if !compareBaseProperties(r, p) { - return false - } - - // If the attestations are not set, we can skip this check - if p.Attestation != nil && r.Statement != nil && p.Attestation.Predicate != nil && r.Statement.Predicate != nil { - if p.Attestation.PredicateType != r.Statement.PredicateType { - return false - } - return reflect.DeepEqual(p.Attestation.Predicate, r.Statement.Predicate) - } - - return true -} - -// compareBaseProperties compares the base properties of the verification result and the server provenance -func compareBaseProperties(r *verify.VerificationResult, p *registry.Provenance) bool { - // Extract the signer identity from the certificate - siIdentity, err := signerIdentityFromCertificate(r.Signature.Certificate) - if err != nil { - slog.Error("error parsing signer identity") - } - // Compare repository name and reference, signer identity, runner environment, and cert issuer - if p.RepositoryURI != "" { - // If the repository URI is set, we need to compare it with the verification result - if p.RepositoryURI != r.Signature.Certificate.SourceRepositoryURI { - return false - } - } - if p.RepositoryRef != "" { - // If the repository reference is set, we need to compare it with the verification result - if p.RepositoryRef != r.Signature.Certificate.SourceRepositoryRef { - return false - } - } - if p.RunnerEnvironment != "" { - // If the runner environment is set, we need to compare it with the verification result - if p.RunnerEnvironment != r.Signature.Certificate.RunnerEnvironment { - return false - } - } - if p.CertIssuer != "" { - // If the certificate issuer is set, we need to compare it with the verification result - if p.CertIssuer != r.Signature.Certificate.Issuer { - return false - } - } - if p.SignerIdentity != "" { - // If the signer identity is set, we need to compare it with the verification result - if p.SignerIdentity != siIdentity { - return false - } - } - return true -} - -// signerIdentityFromCertificate returns the signer identity. When the identity -// is a URI (from the BuildSignerURI extension or the cert SAN), we return only -// the URI path component. We split it this way to ensure we can make rules -// more generalizable (applicable to the same path regardless of the repo for example). -func signerIdentityFromCertificate(c *certificate.Summary) (string, error) { - var builderURL string - - if c.SubjectAlternativeName == "" { - return "", fmt.Errorf("certificate has no signer identity in SAN (is it a fulcio cert?)") - } - - switch { - case c.SubjectAlternativeName != "": - builderURL = c.SubjectAlternativeName - default: - // Return the SAN in the cert as a last resort. This handles the case when - // we don't have a signer identity but also when the SAN is an email - // when a user authenticated using an OIDC provider or a SPIFFE ID. - // Any other SAN types are returned verbatim - return c.SubjectAlternativeName, nil - } - - // Any signer identity not issued by github actions is returned verbatim - if c.Issuer != githubTokenIssuer { - return builderURL, nil - } - - // When handling a cert issued through GitHub actions tokens, break the identity - // into its components. The verifier captures the git reference and the - // the repository URI. - if c.SourceRepositoryURI == "" { - return "", fmt.Errorf( - "certificate extension dont have a SourceRepositoryURI set (oid 1.3.6.1.4.1.57264.1.5)", - ) - } - - builderURL, _, _ = strings.Cut(builderURL, "@") - builderURL = strings.TrimPrefix(builderURL, c.SourceRepositoryURI) - - return builderURL, nil -} diff --git a/pkg/runner/retriever/retriever.go b/pkg/runner/retriever/retriever.go index 8f824575c4..607765b48c 100644 --- a/pkg/runner/retriever/retriever.go +++ b/pkg/runner/retriever/retriever.go @@ -13,12 +13,12 @@ import ( nameref "github.com/google/go-containerregistry/pkg/name" + "github.com/stacklok/toolhive-core/container/verifier" "github.com/stacklok/toolhive-core/httperr" types "github.com/stacklok/toolhive-core/registry/types" "github.com/stacklok/toolhive/pkg/config" "github.com/stacklok/toolhive/pkg/container/images" "github.com/stacklok/toolhive/pkg/container/templates" - "github.com/stacklok/toolhive/pkg/container/verifier" "github.com/stacklok/toolhive/pkg/registry" "github.com/stacklok/toolhive/pkg/runner" ) @@ -345,7 +345,7 @@ func verifyImage(image string, server *types.ImageMetadata, verifySetting string slog.Warn("Image verification is disabled") case VerifyImageWarn, VerifyImageEnabled: // Create a new verifier - v, err := verifier.New(server) + v, err := verifier.New(server, images.NewCompositeKeychain()) if err != nil { // This happens if we have no provenance entry in the registry for this server. // Not finding provenance info in the registry is not a fatal error if the setting is "warn". From 742a78904ec66857d5204852b61b63370c434d66 Mon Sep 17 00:00:00 2001 From: Radoslav Dimitrov Date: Sat, 21 Feb 2026 17:14:50 +0200 Subject: [PATCH 2/2] Bump toolhive-catalog Signed-off-by: Radoslav Dimitrov --- go.mod | 3 +-- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index 77f662c469..aa6d08583d 100644 --- a/go.mod +++ b/go.mod @@ -42,7 +42,7 @@ require ( github.com/prometheus/client_golang v1.23.2 github.com/redis/go-redis/v9 v9.18.0 github.com/spf13/viper v1.21.0 - github.com/stacklok/toolhive-catalog v0.0.0-20260220124241-3f98ae192446 + github.com/stacklok/toolhive-catalog v0.0.0-20260221151009-65da3691ed08 github.com/stacklok/toolhive-core v0.0.7 github.com/stretchr/testify v1.11.1 github.com/swaggo/swag/v2 v2.0.0-rc5 @@ -181,7 +181,6 @@ require ( github.com/in-toto/in-toto-golang v0.9.0 // indirect github.com/invopop/jsonschema v0.13.0 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect - github.com/jedisct1/go-minisign v0.0.0-20230811132847-661be99b8267 // indirect github.com/jpillora/backoff v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect diff --git a/go.sum b/go.sum index 29ebb5ec75..2bd4375a1e 100644 --- a/go.sum +++ b/go.sum @@ -798,8 +798,8 @@ github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= -github.com/stacklok/toolhive-catalog v0.0.0-20260220124241-3f98ae192446 h1:dRQhDz/3C9VUI9x5/8ofvLJhW+iJuS2JJEt72lCHw44= -github.com/stacklok/toolhive-catalog v0.0.0-20260220124241-3f98ae192446/go.mod h1:mdpmpFak09Idzp1v5zTwjL9caj1pEdHlkPppFa2odPw= +github.com/stacklok/toolhive-catalog v0.0.0-20260221151009-65da3691ed08 h1:6R4b9JMpVlBx7PpkoHphQf6nymrFgy5v1lOdKmzZy28= +github.com/stacklok/toolhive-catalog v0.0.0-20260221151009-65da3691ed08/go.mod h1:pRmjVHQU2pIqKctbsFErmFmADAXfeIjIlQUOgZMVUiw= github.com/stacklok/toolhive-core v0.0.7 h1:AW0dmSDOJjUCynlUQMXH+ODNeqPWRHf370gkvRdMO+E= github.com/stacklok/toolhive-core v0.0.7/go.mod h1:LhtXDsHbj5CcNKBJDzPAA2Y4fmYAEVDFSGlGuLkAsK4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=