From 88f2d8092bcac4b285634cddffd8396a696efbe0 Mon Sep 17 00:00:00 2001 From: Samuele Verzi Date: Mon, 24 Aug 2026 16:41:15 +0200 Subject: [PATCH 1/6] Bump toolhive-core to v0.0.42 Needed for the Skill.Provenance field (#6310 prerequisite) landing in the next commit. v0.0.42 also migrated toolhive-core's cel package to the renamed cel.dev/cel-go module; update the two callers that pass it EnvOptions to match, or the build breaks. Signed-off-by: Samuele Verzi --- go.mod | 82 +++--- go.sum | 238 +++++++++--------- pkg/auth/awssts/role_mapper.go | 2 +- .../tokenexchange/multi_issuer_validator.go | 2 +- 4 files changed, 166 insertions(+), 158 deletions(-) diff --git a/go.mod b/go.mod index e61aa40cc0..7e212d90dd 100644 --- a/go.mod +++ b/go.mod @@ -7,9 +7,9 @@ require ( github.com/1password/onepassword-sdk-go v0.3.1 github.com/alicebob/miniredis/v2 v2.38.0 github.com/atotto/clipboard v0.1.4 - github.com/aws/aws-sdk-go-v2 v1.43.5 - github.com/aws/aws-sdk-go-v2/config v1.32.36 - github.com/aws/aws-sdk-go-v2/service/sts v1.45.5 + github.com/aws/aws-sdk-go-v2 v1.43.7 + github.com/aws/aws-sdk-go-v2/config v1.32.38 + github.com/aws/aws-sdk-go-v2/service/sts v1.45.7 github.com/cedar-policy/cedar-go v1.8.0 github.com/cenkalti/backoff/v5 v5.0.3 github.com/charmbracelet/bubbles v1.0.0 @@ -20,13 +20,12 @@ require ( github.com/coreos/go-oidc/v3 v3.20.0 github.com/docker/go-connections v0.7.0 // indirect github.com/evanphx/json-patch/v5 v5.9.11 - github.com/go-chi/chi/v5 v5.3.0 + github.com/go-chi/chi/v5 v5.3.1 github.com/go-git/go-billy/v5 v5.9.0 github.com/go-git/go-git/v5 v5.19.2 github.com/go-jose/go-jose/v3 v3.0.5 github.com/go-jose/go-jose/v4 v4.1.4 github.com/gofrs/flock v0.13.0 - github.com/google/cel-go v0.31.0 github.com/google/go-cmp v0.7.0 github.com/google/go-containerregistry v0.21.9 github.com/google/uuid v1.6.0 @@ -49,8 +48,8 @@ require ( github.com/shirou/gopsutil/v4 v4.26.7 github.com/spf13/viper v1.21.0 github.com/stacklok/toolhive-catalog v0.20260817.0 - github.com/stacklok/toolhive-core v0.0.41 - github.com/stretchr/testify v1.11.1 + github.com/stacklok/toolhive-core v0.0.42 + github.com/stretchr/testify v1.12.1 github.com/swaggo/swag/v2 v2.0.0-rc5 github.com/tailscale/hujson v0.0.0-20260727124030-b80ff77dac4f github.com/testcontainers/testcontainers-go v0.42.0 @@ -85,25 +84,29 @@ require github.com/getsentry/sentry-go/otel v0.44.1 require github.com/hashicorp/golang-lru/v2 v2.0.7 require ( + cel.dev/cel-go v0.32.0 github.com/github/smimesign v0.2.0 go.starlark.net v0.0.0-20260630144053-529d8e869a14 ) require ( - github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.37 // indirect - github.com/go-openapi/runtime/server-middleware v0.30.0 // indirect - github.com/go-openapi/swag/pools v0.27.3 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.39 // indirect + github.com/go-openapi/runtime/server-middleware v0.33.1 // indirect + github.com/go-openapi/swag/pools v0.28.0 // indirect github.com/jedisct1/go-minisign v0.0.0-20241212093149-d2f9f49435c7 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c // indirect github.com/modelcontextprotocol/go-sdk v1.7.0 // indirect - github.com/oklog/ulid/v2 v2.1.1 // indirect + github.com/oklog/ulid/v2 v2.1.2 // indirect github.com/oleiade/reflections v1.1.0 // indirect - github.com/sassoftware/relic v7.2.1+incompatible // indirect + github.com/sassoftware/relic/v8 v8.2.0 // indirect github.com/segmentio/encoding v0.5.4 // indirect github.com/theupdateframework/go-tuf v0.7.0 // indirect github.com/valyala/fastjson v1.6.10 // indirect github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.21.0 // indirect + go.opentelemetry.io/otel/log v0.21.0 // indirect + go.opentelemetry.io/otel/sdk/log v0.21.0 // indirect ) require ( @@ -114,19 +117,18 @@ require ( github.com/ProtonMail/go-crypto v1.4.1 // indirect github.com/antlr4-go/antlr/v4 v4.13.1 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect - github.com/aws/aws-sdk-go-v2/credentials v1.19.35 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.36 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.36 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.36 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.16 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.36 // indirect - github.com/aws/aws-sdk-go-v2/service/signin v1.5.5 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.33.5 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.38.5 // indirect - github.com/aws/smithy-go v1.27.7 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.19.37 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.38 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.38 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.38 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.17 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.38 // indirect + github.com/aws/aws-sdk-go-v2/service/signin v1.5.7 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.33.7 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.38.7 // indirect + github.com/aws/smithy-go v1.27.8 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect - github.com/blang/semver v3.5.1+incompatible // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/charmbracelet/colorprofile v0.4.1 // indirect @@ -163,27 +165,27 @@ require ( github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-logr/zapr v1.3.0 // indirect github.com/go-ole/go-ole v1.3.0 // indirect - github.com/go-openapi/analysis v0.25.5 // indirect + github.com/go-openapi/analysis v0.26.0 // indirect github.com/go-openapi/errors v0.22.8 // indirect github.com/go-openapi/jsonpointer v1.0.0 // indirect github.com/go-openapi/jsonreference v1.0.0 // indirect - github.com/go-openapi/loads v0.25.0 // indirect - github.com/go-openapi/runtime v0.33.0 // indirect + github.com/go-openapi/loads v0.25.1 // indirect + github.com/go-openapi/runtime v0.33.1 // indirect github.com/go-openapi/spec v0.22.9 // indirect github.com/go-openapi/strfmt v0.27.0 // indirect github.com/go-openapi/swag v0.26.1 // indirect github.com/go-openapi/swag/cmdutils v0.27.0 // indirect - github.com/go-openapi/swag/conv v0.28.0 // indirect - github.com/go-openapi/swag/fileutils v0.27.3 // indirect + github.com/go-openapi/swag/conv v0.29.1 // indirect + github.com/go-openapi/swag/fileutils v0.28.0 // indirect github.com/go-openapi/swag/jsonname v0.26.1 // indirect - github.com/go-openapi/swag/jsonutils v0.27.3 // indirect - github.com/go-openapi/swag/loading v0.27.3 // indirect - github.com/go-openapi/swag/mangling v0.27.3 // indirect + github.com/go-openapi/swag/jsonutils v0.28.0 // indirect + github.com/go-openapi/swag/loading v0.28.0 // indirect + github.com/go-openapi/swag/mangling v0.28.0 // indirect github.com/go-openapi/swag/netutils v0.27.0 // indirect - github.com/go-openapi/swag/stringutils v0.27.3 // indirect - github.com/go-openapi/swag/typeutils v0.28.0 // indirect - github.com/go-openapi/swag/yamlutils v0.27.3 // indirect - github.com/go-openapi/validate v0.26.1 // indirect + github.com/go-openapi/swag/stringutils v0.28.0 // indirect + github.com/go-openapi/swag/typeutils v0.29.1 // indirect + github.com/go-openapi/swag/yamlutils v0.28.0 // indirect + github.com/go-openapi/validate v0.26.3 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/go-viper/mapstructure/v2 v2.5.0 // indirect github.com/gobuffalo/pop/v6 v6.1.1 // indirect @@ -252,12 +254,12 @@ require ( github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sagikazarmark/locafero v0.12.0 // indirect github.com/seatgeek/logrus-gelf-formatter v0.0.0-20210414080842-5b05eb8ff761 // indirect - github.com/secure-systems-lab/go-securesystemslib v0.11.0 // indirect + github.com/secure-systems-lab/go-securesystemslib v0.11.1 // indirect github.com/sergi/go-diff v1.4.0 // indirect github.com/sethvargo/go-retry v0.3.0 // indirect github.com/shibumi/go-pathspec v1.3.0 // indirect - github.com/sigstore/protobuf-specs v0.5.1 // indirect - github.com/sigstore/rekor v1.5.3 // indirect + github.com/sigstore/protobuf-specs v0.5.2 // indirect + github.com/sigstore/rekor v1.5.4 // indirect github.com/sigstore/rekor-tiles/v2 v2.3.0 // indirect github.com/sigstore/sigstore v1.10.9 github.com/sigstore/sigstore-go v1.3.0 @@ -297,7 +299,7 @@ require ( go.uber.org/atomic v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.yaml.in/yaml/v2 v2.4.4 // indirect - go.yaml.in/yaml/v3 v3.0.4 // indirect + go.yaml.in/yaml/v3 v3.0.5 // indirect golang.ngrok.com/muxado/v2 v2.0.1 // indirect golang.org/x/exp/event v0.0.0-20260611194520-c48552f49976 // indirect golang.org/x/net v0.58.0 // indirect @@ -308,7 +310,7 @@ require ( google.golang.org/genproto/googleapis/api v0.0.0-20260803160001-6ac0973c030d // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260803160001-6ac0973c030d // indirect google.golang.org/grpc v1.83.0 // indirect - google.golang.org/protobuf v1.36.11 + google.golang.org/protobuf v1.36.12 gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect diff --git a/go.sum b/go.sum index e6e1d95ab2..416d65eb9d 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +cel.dev/cel-go v0.32.0 h1:irvpFKr5EuGPyxeME03ERh0rii1TX+BDAnB9eL3IvNk= +cel.dev/cel-go v0.32.0/go.mod h1:DnVip7tpJSsgZymwfT+m1tnEVy3ivAjSMXPx12YrMkU= cel.dev/expr v0.25.2 h1:K6j46C81hXtZQfuX60cVWQFBJahKSE2gfRbNuvr5bFs= cel.dev/expr v0.25.2/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE= @@ -8,8 +10,8 @@ cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIi cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= -cloud.google.com/go/iam v1.11.0 h1:KieQ9Pb+LLPak1O3Rv3GgCxhnmkYf7Xyh0P5HfF1jFM= -cloud.google.com/go/iam v1.11.0/go.mod h1:KP+nKGugNJW4LcLx1uEZcq1ok5sQHFaQehQNl4QDgV4= +cloud.google.com/go/iam v1.13.0 h1:ufT3FPT5rFFXu6UtLkNoxaOaV5EuA1dsSkmemCSTo6U= +cloud.google.com/go/iam v1.13.0/go.mod h1:gHXdDEiPDvqd1q1KwBDGQlgZY/BwY760zU2LhOZS5w0= cloud.google.com/go/kms v1.33.0 h1:pG0X78m212b2pv9N4fdMoUO69LuZGQ9kSvn8sHBOFAo= cloud.google.com/go/kms v1.33.0/go.mod h1:CSGvW6GnMQbY+1nOHcIzhMtHSbExXlOmCKjWtYVjcpA= cloud.google.com/go/longrunning v1.2.0 h1:WjYH3YHBGCxGJP9M4dWGHBfXr/cFIjMkNgWcJj7/iMM= @@ -65,43 +67,41 @@ github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3d github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= -github.com/aws/aws-sdk-go-v2 v1.43.5 h1:yKT5GYnFWhuDo+DqKvE5ZPwVn3RjC4MAeBtZGlh6AVM= -github.com/aws/aws-sdk-go-v2 v1.43.5/go.mod h1:wZjAJppCntyOGgVSmgVTfDyRJK5PHOasO6Wsy8U7Axk= -github.com/aws/aws-sdk-go-v2/config v1.32.36 h1:mX6ietU7UlB4w/2IUaexJdsyUDvhTd+jYPjVePiyi6s= -github.com/aws/aws-sdk-go-v2/config v1.32.36/go.mod h1:rMpV4xk7ZK59edraSaHP0jsWrztWTT5tbCwWY495hug= -github.com/aws/aws-sdk-go-v2/credentials v1.19.35 h1:Cxua2RVdRwL0sfjHM/SnQoOnQ7xKng9m5EQBO8BnZlg= -github.com/aws/aws-sdk-go-v2/credentials v1.19.35/go.mod h1:9XQ+RSIGPkycr+oCJYnB1uTv5kMVVR+rd2vYK0Hxj2w= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.36 h1:gucL1KH/PAYbpTpBg09CiVpBdTu4qkCl8C7xOTBixUg= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.36/go.mod h1:usTB+PHhNMhrx2dxUeHcM7OrT5pySvmjYI++IsefPN0= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.36 h1:5CrzwxDqf4w3x1Vs3/NiZ0nsC34Hbm3pIDMWbsLebOE= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.36/go.mod h1:A3gHdKZIvG/QXERzZwcxNS3RNDFcRCuhhTFBYp+V/nw= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.36 h1:A4N2f4YPcST0v+dWtX+xrpPPCL9VTBhoIFFUWYqbacE= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.36/go.mod h1:B/Qr859uxWUEfZeGotK5KAEoof4Q9YWgNtPSwV6jcyk= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.37 h1:oyd3ke4V9AhKcRR7rRgxk1VyI+DjK2CBQtbxh3OkdaA= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.37/go.mod h1:aA9D7SqfG9IC1b7FLD7Iyc8Q4JN0a8gHhNjN4zPlIaI= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.16 h1:iE4NGbvqUZnHDqddQAauZzCILYtFjOHwRM5MOOKLB5A= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.16/go.mod h1:VsjEgrP+ibcou8TlWA4tYaB+0OojuhirsmCe+U60hTA= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.36 h1:fx2ujmozWn+C/GtfXfz5k6Ckzza40ElOpIW7d92fLWQ= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.36/go.mod h1:QT2ufGVJ+xTRxtXPHTQ1kHkAdWIKPCmD+BqYAXWv8/4= -github.com/aws/aws-sdk-go-v2/service/kms v1.52.0 h1:QNtg+Mtj1zmepk568+UKBD5DFfqh+ESTUUqQT27JkQc= -github.com/aws/aws-sdk-go-v2/service/kms v1.52.0/go.mod h1:Y0+uxvxz6ib4KktRdK0V4X45Vcs/JyYoz8H71pO8xeI= -github.com/aws/aws-sdk-go-v2/service/signin v1.5.5 h1:0VTFBfOgPJrUSpGMgzoi8qLcXF5dbmiBuxpo14eBWUw= -github.com/aws/aws-sdk-go-v2/service/signin v1.5.5/go.mod h1:sNZYlBxoohYMBYl47BO/bFtAM6I8HSsPa1qwwPPRGoQ= -github.com/aws/aws-sdk-go-v2/service/sso v1.33.5 h1:jDQARFp1mJ2PEnllQf01nfFXGfWMJ59e0/HCHUTTZCk= -github.com/aws/aws-sdk-go-v2/service/sso v1.33.5/go.mod h1:OcT2AhgTuxGAwZk5hgxaNLGpS33W8s8dUQadGVDVY9I= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.38.5 h1:8xo1q9ttkYqMJ6vOXX67FPSpVEI7BWKVTKh77g82w+8= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.38.5/go.mod h1:hbBeEUrZg6VddXYZpbKPyF0tl4XEnM+Dbx92RW3vmZI= -github.com/aws/aws-sdk-go-v2/service/sts v1.45.5 h1:eQ5BtXDrPg2wK0AjtVPzeBhUpYPeqHE/ptiH7xJRGek= -github.com/aws/aws-sdk-go-v2/service/sts v1.45.5/go.mod h1:f9ImhnOISY7BuTZLM8qHepCYnglHBVLk5wVzatmP++w= -github.com/aws/smithy-go v1.27.7 h1:Zgj5z4LfcDYoQIVk+n/yGdTkP/2y6ZT5vYxe0fp7bqE= -github.com/aws/smithy-go v1.27.7/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= +github.com/aws/aws-sdk-go-v2 v1.43.7 h1:msCzvkeYJA9ehbV8mRRmkZLo/zJg/+yDVLNtflg83hQ= +github.com/aws/aws-sdk-go-v2 v1.43.7/go.mod h1:tXpPM+v0D1lndmga+HqqLDIzUFJlEeR21aspVklHF00= +github.com/aws/aws-sdk-go-v2/config v1.32.38 h1:n4yPHBjtQ3BrIIUyk0/LAqf/BL2iv0Tw6XZcMRzM0ps= +github.com/aws/aws-sdk-go-v2/config v1.32.38/go.mod h1:dencYsOS1R7rBy8zehCvwBYzdxxL4Q/nRK7In03wjN8= +github.com/aws/aws-sdk-go-v2/credentials v1.19.37 h1:FJ8Iz4/xISMB/rwLlgfWujfGDFWr0oneQgtA6KPcYLY= +github.com/aws/aws-sdk-go-v2/credentials v1.19.37/go.mod h1:Q6pWOgVUp49x4g5QVi29wHofUoICnZ+Zq4jHbRN/7ec= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.38 h1:Nqo2jU1wz5rnBM9XQyXfVD1RP8txkbP3EDx8hR/hbCE= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.38/go.mod h1:PzJFHhjR2vWFKHe8HmY5Lxhvwyxnr5MERtk0nDxWNbk= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.38 h1:MBMg0zJ6i4TkAJ0dVFLKKn2cOkY6FkicmUDM67BRr6g= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.38/go.mod h1:9MWuJbyiUyj6eA7W1/zm1zuePDPSB3g+xcgRQeMWsXc= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.38 h1:lHm4jPf3k1Lz5ZWc+Vcn3MKVwym+26kWCba9FkJ4f0Y= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.38/go.mod h1:Rn+P2XR+FbyZzjmWKjg/KUZNxmGfr5oZwh5jQiE+CzI= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.39 h1:vo4xvMRs/F6h1E52qsgLqCQgWIQXgIJUauG6rlZEh4U= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.39/go.mod h1:jB03R1ij/A+OE2e1dz6vgj076gd7vlYcfstAzj3HcnU= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.17 h1:OvYZOB3qA6zvfdRFiRFRzVSiElMYrz3GdntkXZxlp1o= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.17/go.mod h1:JgR/2Ew50ACfIWau1oeMRX59tMtC0kM+PYQGEaT04cY= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.38 h1:H/5TI1jqaHsNoDQ60UwvPvJBg4GURkinXI3Qga29t2w= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.38/go.mod h1:PTVFf+XH++7NJOky+RLBYQx0QA5NcaeEYFQ2fsi0nwo= +github.com/aws/aws-sdk-go-v2/service/kms v1.55.0 h1:uB8ymkVosyourmGXCZHyWhJ4wuKA4xq3ii2dVMPtBZY= +github.com/aws/aws-sdk-go-v2/service/kms v1.55.0/go.mod h1:rK4RITSY/qJw3qVJ7p19fceOWuvrisqqOChFkX05n5I= +github.com/aws/aws-sdk-go-v2/service/signin v1.5.7 h1:YcczQ6zNH/ojIzD/ikDrO+RfW06wmdMp18d4NH5hXY4= +github.com/aws/aws-sdk-go-v2/service/signin v1.5.7/go.mod h1:nl9RVnb9ulgAYzOkjLq1NyFxmWcnH2maCUEuOdESy98= +github.com/aws/aws-sdk-go-v2/service/sso v1.33.7 h1:P+bMNiA93gyuYT3Oh+4dWtvrnGcu2bd9Uy5hRJM8BNo= +github.com/aws/aws-sdk-go-v2/service/sso v1.33.7/go.mod h1:zy+397isDFLvleg9H18Zq2MGzMso7uKyJyzR7DWSgFk= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.38.7 h1:WWkehGZ4nWtOKLMy0yi8+RqzzVqAGe60hGaxwF06JAw= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.38.7/go.mod h1:T8AI4SbQYm9ybcVmki2T3n7Qg1g3kfWoeQlNwNYOyO8= +github.com/aws/aws-sdk-go-v2/service/sts v1.45.7 h1:yU/9y2r7s9kSUPbHXbpQTa4LA8kt+CMgpu1OBrhx8p4= +github.com/aws/aws-sdk-go-v2/service/sts v1.45.7/go.mod h1:0lQTDEBArMevQXpxu443LVGjKxxEeSsSnrw9n8YiTMg= +github.com/aws/smithy-go v1.27.8 h1:FR0dxZfIlV7Z8eh2iHfIofdunw382XsDV3Mxt9nUvRY= +github.com/aws/smithy-go v1.27.8/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= -github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= @@ -238,8 +238,8 @@ github.com/gkampitakis/go-snaps v0.5.15 h1:amyJrvM1D33cPHwVrjo9jQxX8g/7E2wYdZ+01 github.com/gkampitakis/go-snaps v0.5.15/go.mod h1:HNpx/9GoKisdhw9AFOBT1N7DBs9DiHo/hGheFGBZ+mc= github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= -github.com/go-chi/chi/v5 v5.3.0 h1:halUjDxhshgXHMrao5bB8eNBXo/rnzwr8m5m36glehM= -github.com/go-chi/chi/v5 v5.3.0/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutVsOCto= +github.com/go-chi/chi/v5 v5.3.1 h1:3j4HZLGZQ3JpMCrPJF/Jl3mYJfWLKBfNJ6quurUGCf8= +github.com/go-chi/chi/v5 v5.3.1/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutVsOCto= github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= @@ -266,20 +266,20 @@ github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= -github.com/go-openapi/analysis v0.25.5 h1:xPYEvTb90o1y0epuiOPAoG4QqahjP3cdp5xNlHeKJRI= -github.com/go-openapi/analysis v0.25.5/go.mod h1:d3UGtQC5uq5Kqqqis2VH09Km/v3vwsWrYkbp4gdm+Rc= +github.com/go-openapi/analysis v0.26.0 h1:1xECln1iMMmQnTjgcknC1vi1hA4KISt6IHpSwnqcuwI= +github.com/go-openapi/analysis v0.26.0/go.mod h1:40gERFi/2dyXA1FaqRRLxkv1IlC6X+GPDNd1xrYAjZE= github.com/go-openapi/errors v0.22.8 h1:oP7sW7TWc3wFFjrzzj0nI83H2qMBkNjNfSd+XRejk/I= github.com/go-openapi/errors v0.22.8/go.mod h1:BuUoHcYrU6E7V9gfj1I5wLQqgtIHnup/alXZ8KdgQ0w= github.com/go-openapi/jsonpointer v1.0.0 h1:kR9tHqY0CtZaOPVFm622dPVNhrvYpwr4uCxgL3h1H8s= github.com/go-openapi/jsonpointer v1.0.0/go.mod h1:Z3rw7dWu1p9IgitXCFamSlA5lmDiklEB6vkaxcNZW5Y= github.com/go-openapi/jsonreference v1.0.0 h1:jlmTr6torcd1YgDQvSfNmRtKzYDO4FGBkrAdlAVWnpY= github.com/go-openapi/jsonreference v1.0.0/go.mod h1:jtwdyGbJk0Xhe5Y+rwtglQP6Sb1WZST4rT32LWB+sv0= -github.com/go-openapi/loads v0.25.0 h1:74Bc2snfaVlsHzwdQj/3gsA9XJz3daXTJVs+4ZaK7jI= -github.com/go-openapi/loads v0.25.0/go.mod h1:JFBw4SIB9+PTIFHDfcXuSSy5h6aWzjtUCrPYyx3qWU8= -github.com/go-openapi/runtime v0.33.0 h1:Dd3Oj2ig+WH8ckK95l0Wn2V8a4bH/UqWPRZVT0vc8yU= -github.com/go-openapi/runtime v0.33.0/go.mod h1:+rsupH3+TFKqmFysqkmgBOTxpVJV8eV+j9myvvea2Xw= -github.com/go-openapi/runtime/server-middleware v0.30.0 h1:8rPoJ/xv7JL8BsovaqboKETlpWBArVh8n+0L/GyePog= -github.com/go-openapi/runtime/server-middleware v0.30.0/go.mod h1:OYNT/TxNvB/VK5oe4htM2jDTwlEXuejVJmu0DVZfAMs= +github.com/go-openapi/loads v0.25.1 h1:toKQdIDLxlqfKLLGUUmUsiTd5/X0Chzvde9EGYQP/Ac= +github.com/go-openapi/loads v0.25.1/go.mod h1:33Hen4tsKXHL45TyYojvfD5fZUFN4O1y4r/XhsRW2zc= +github.com/go-openapi/runtime v0.33.1 h1:jCvhI+wAdsn29byy+RgcPcg+j39YT6E304QOE/WqIVk= +github.com/go-openapi/runtime v0.33.1/go.mod h1:Dl5SMVRnJz+d8bX6Y1zxy0QKpqe/ysvVeUEh1nCpEZ4= +github.com/go-openapi/runtime/server-middleware v0.33.1 h1:IAeKbwWnBnpsYTpuPVS8t73ZrPpKvRZnK2iJ2KJGUV0= +github.com/go-openapi/runtime/server-middleware v0.33.1/go.mod h1:2Gej5fDxqeJxY+w38vxXYW0BgFASfgBsJ5rXwN1Fseg= github.com/go-openapi/spec v0.22.9 h1:/vKIFDcGKp0ktZWGbym/tJEWbk6/XOEmAVU0kqKMH+w= github.com/go-openapi/spec v0.22.9/go.mod h1:b/mNUYIOQOyIiUzUzXEE8xzyZqf93KvM9hQGP91yfl0= github.com/go-openapi/strfmt v0.27.0 h1:kbcTeaD9TXuXD0hhMXzuYa1sdTo6+dWGvwjW93E80IM= @@ -288,36 +288,36 @@ github.com/go-openapi/swag v0.26.1 h1:l5sVEyVpwj+DDYeZyo7wQI/Ebn/mKYIyGB/pFwAfGo github.com/go-openapi/swag v0.26.1/go.mod h1:yNY38BbIVthxbkDtq1UHBCGasBqjakW3lCR6ANzdBEw= github.com/go-openapi/swag/cmdutils v0.27.0 h1:aIKiqhB29AaP+7xm8/CPg3uOpeHx2SUp6TvMpu/a31Y= github.com/go-openapi/swag/cmdutils v0.27.0/go.mod h1:Sm1MVFMkF6guJJ+pQqHnQA3N0j9qALV3NxzDSv6bETM= -github.com/go-openapi/swag/conv v0.28.0 h1:GtqqbyFe7vR5Y7ehxG9W6/OvrSFdf1OLeTGp40TqxH8= -github.com/go-openapi/swag/conv v0.28.0/go.mod h1:mbUE+mzctnhxi864m0Q07SpN8OowD9JhxmxuYvZZD/k= -github.com/go-openapi/swag/fileutils v0.27.3 h1:3UVoZ2RLaIs1lt+2jcKzL8RM3Yk0rmsDE9FLA/HGxFE= -github.com/go-openapi/swag/fileutils v0.27.3/go.mod h1:VvJFZLTZS0AI854gEQz5tk7dBESdLjiNUMSZ/th2ry8= +github.com/go-openapi/swag/conv v0.29.1 h1:AC4Eh/5c/eUDOUCzzsRC9ghmFgOSBHeRMGIngY0ZUGA= +github.com/go-openapi/swag/conv v0.29.1/go.mod h1:S1X7/ZrBEZOC0Wc8AGxjbcGS92l3WEjA7aPtpl+RaqM= +github.com/go-openapi/swag/fileutils v0.28.0 h1:Z04XWQD7R8Eq+7GnOrjovBxPPmZzsS4gt2H2GPGIViU= +github.com/go-openapi/swag/fileutils v0.28.0/go.mod h1:VvJFZLTZS0AI854gEQz5tk7dBESdLjiNUMSZ/th2ry8= github.com/go-openapi/swag/jsonname v0.26.1 h1:VReupaV6WxlAsCn0e4DUfgV6bPmINnPpyJDLqSfNPcE= github.com/go-openapi/swag/jsonname v0.26.1/go.mod h1:OvdW6BoWoj33pTfi7x9vFrgmT+fk7aw0BRwvCE0YOuc= -github.com/go-openapi/swag/jsonutils v0.27.3 h1:1DEz+O82frtSMBcos/7XIn1GnpNTbsD4Bru4Dc/uhRc= -github.com/go-openapi/swag/jsonutils v0.27.3/go.mod h1:qiDCoQvzkMxrV3G8FLEdIU5L+EFYc0zcDOHWT3Yofvo= -github.com/go-openapi/swag/jsonutils/fixtures_test v0.27.3 h1:h/eT9kmGCDdFLJF29lOhzLtF0FmP1AX2MhLJWVebsb8= -github.com/go-openapi/swag/jsonutils/fixtures_test v0.27.3/go.mod h1:mofwUWx70wvskwESqRJ//k/9kURmCgyJl5m5Ppoh5kY= -github.com/go-openapi/swag/loading v0.27.3 h1:L9nQkEgzU7QgFQL+pLEMfGUKxeM4pWwGwbET9Z3weW0= -github.com/go-openapi/swag/loading v0.27.3/go.mod h1:rJ0NeaKsF4CVPnMGjPQl7JlSHzvD0bc2DKXLss1hiuE= -github.com/go-openapi/swag/mangling v0.27.3 h1:gRzzD1PAUoLTtGMgI3KpBmCSOlTuLTFWnviLxLcTnyg= -github.com/go-openapi/swag/mangling v0.27.3/go.mod h1:jtBE2+V+3pILxOR7Vgce+Cwp6A2PgZbvVqfNntbVs0w= +github.com/go-openapi/swag/jsonutils v0.28.0 h1:YIch6FwO7RXzeAnbO8Tu7dWBZeUEH+4nA0HXltVTnv4= +github.com/go-openapi/swag/jsonutils v0.28.0/go.mod h1:CYM3WlTUcagR2ZoHdz54di/cbBqt82tuxuXgAjxw+mg= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.28.0 h1:qV+VVUAx5Oro8WjVWpZeql7YReTKhT4smR4zhcOQZr0= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.28.0/go.mod h1:mofwUWx70wvskwESqRJ//k/9kURmCgyJl5m5Ppoh5kY= +github.com/go-openapi/swag/loading v0.28.0 h1:td8QZdZC9MIYGGSnSPKShKiK22I2tU5UQvuUhIBPRLU= +github.com/go-openapi/swag/loading v0.28.0/go.mod h1:rXB0QiQX5mMveXEA7ouM4KiiM9jVJe4K6BVbwhD1M4k= +github.com/go-openapi/swag/mangling v0.28.0 h1:pH8eyeNO9SLYsTMWJrurnNfKmDa28XrlA+HePVD53VM= +github.com/go-openapi/swag/mangling v0.28.0/go.mod h1:jtBE2+V+3pILxOR7Vgce+Cwp6A2PgZbvVqfNntbVs0w= github.com/go-openapi/swag/netutils v0.27.0 h1:lEUG+hHvPvLggB3A8snFk0IRKNf9uC0YKc+7WYqvAF8= github.com/go-openapi/swag/netutils v0.27.0/go.mod h1:J+WYyFMLtvtCGqa6jLv+YNUmIKI3ZRQRrvfNDMoQoEQ= -github.com/go-openapi/swag/pools v0.27.3 h1:gXjImP3F6/56wRRcFgEPld084Y6u2gs21ikPBt8NKBk= -github.com/go-openapi/swag/pools v0.27.3/go.mod h1:kVQefhSK5RWuRe7BXsL8htgBPAMpN7HDGpGEknqugeE= -github.com/go-openapi/swag/stringutils v0.27.3 h1:Ru28hnbAvN5wycALQYy8IobHvASq+FUFMlp1QzLM0JI= -github.com/go-openapi/swag/stringutils v0.27.3/go.mod h1:lzRN95CxXmA03XcDWHLOb6nOMcxCqR5rGY0lOgsfRoM= -github.com/go-openapi/swag/typeutils v0.28.0 h1:nRBKSBXjDgf01VDPB3fWeD9nQuhCOVeIYAkUx2tbkyY= -github.com/go-openapi/swag/typeutils v0.28.0/go.mod h1:Srm0xFNRZ1Y+vCxJclo5qzx8aj+1pAKda/YfFPrG0dQ= -github.com/go-openapi/swag/yamlutils v0.27.3 h1:cRFCAoYtslYn9L9T0xWryHy1t7c1MACC+DMj3CLvwvs= -github.com/go-openapi/swag/yamlutils v0.27.3/go.mod h1:6JYBGj8sw/NawMllyZY+cTA8Mzk2etS3ZBASdcyPsiU= -github.com/go-openapi/testify/enable/yaml/v2 v2.6.0 h1:gGHwAJ0R/5jU8BEGDbfRNR3hL68dAVi84WuOApp29B0= -github.com/go-openapi/testify/enable/yaml/v2 v2.6.0/go.mod h1:tY+St1SGq4NFl0QIqdTY4aEdbChAHxhyB77XQi9iJCo= -github.com/go-openapi/testify/v2 v2.6.0 h1:5PKH2HE7YJ/LuRPQGvSxBRlFXNQhSetBLlGAgUEu3ug= -github.com/go-openapi/testify/v2 v2.6.0/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw= -github.com/go-openapi/validate v0.26.1 h1:pZSbvtRO8G2R2FpWTYRn3w8LrsNwbtaVhP2dWiBa0Us= -github.com/go-openapi/validate v0.26.1/go.mod h1:B8UMgXiQiwwQWIbmuROlwJZDPGlikPuh7iHV1vPX9Oo= +github.com/go-openapi/swag/pools v0.28.0 h1:HPMZWSAfce3rdVTFcjFiCIBtDg9h4x2QlRrHipwhxeU= +github.com/go-openapi/swag/pools v0.28.0/go.mod h1:kVQefhSK5RWuRe7BXsL8htgBPAMpN7HDGpGEknqugeE= +github.com/go-openapi/swag/stringutils v0.28.0 h1:ixsc9iYgDPubHL/8nSkbnryEHpD2VRlBMLKpQyPXcDU= +github.com/go-openapi/swag/stringutils v0.28.0/go.mod h1:lzRN95CxXmA03XcDWHLOb6nOMcxCqR5rGY0lOgsfRoM= +github.com/go-openapi/swag/typeutils v0.29.1 h1:Nzv9nhnlLCRBPQqfOX+7lB6Guju370or8StT+lIOf6M= +github.com/go-openapi/swag/typeutils v0.29.1/go.mod h1:hxpgDZJVBkBsi/d3MIUosafoFdE5exaQRmVp0zwu3YE= +github.com/go-openapi/swag/yamlutils v0.28.0 h1:TV3JXH6DS46KUroDtMLAYHGkdWf5VDq3wVWFirmzROY= +github.com/go-openapi/swag/yamlutils v0.28.0/go.mod h1:x0q/yndZHEgk9Rx3DyDqzFUmHy55KTvIZldvF2dTJXs= +github.com/go-openapi/testify/enable/yaml/v2 v2.6.1 h1:Jm+/ze2rMtbD98yen92AhATGLGREDYXG56Xr4gMjEtE= +github.com/go-openapi/testify/enable/yaml/v2 v2.6.1/go.mod h1:YDPnwCRDu38/oJBVMBVXOUDiJ9cIeBHWvfImHaXqnv4= +github.com/go-openapi/testify/v2 v2.6.1 h1:6CNJhTjMzgaeaH8WhshcsZNPIvRemiOcFpU7seO/y7Q= +github.com/go-openapi/testify/v2 v2.6.1/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw= +github.com/go-openapi/validate v0.26.3 h1:OkfZgLvLDnGP2hrRGD+42WBiPWWkoHomTJ+IVI+KaDc= +github.com/go-openapi/validate v0.26.3/go.mod h1:7DOOa4raU6NRe7A8VQSKbm3VcuUIioREYHFt+er9Sk8= github.com/go-rod/rod v0.116.2 h1:A5t2Ky2A+5eD/ZJQr1EfsQSe5rms5Xof/qj296e+ZqA= github.com/go-rod/rod v0.116.2/go.mod h1:H+CMO9SCNc2TJ2WfrG+pKhITz57uGNYU43qYHh438Mg= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= @@ -371,8 +371,6 @@ github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= -github.com/google/cel-go v0.31.0 h1:H0bhpFTqOvmHrBGrWKp7ZlhBm5Hh8PYUEXnwxT1LL7A= -github.com/google/cel-go v0.31.0/go.mod h1:X0bD6iVNR8pkROSOoHVdgTkzmRcosof7WQqCD6wcMc8= github.com/google/certificate-transparency-go v1.3.3 h1:hq/rSxztSkXN2tx/3jQqF6Xc0O565UQPdHrOWvZwybo= github.com/google/certificate-transparency-go v1.3.3/go.mod h1:iR17ZgSaXRzSa5qvjFl8TnVD5h8ky2JMVio+dzoKMgA= github.com/google/gnostic-models v0.7.1 h1:SisTfuFKJSKM5CPZkffwi6coztzzeYUhc3v4yxLWH8c= @@ -396,15 +394,16 @@ github.com/google/trillian v1.7.3 h1:hziW+vo4czis48tzx2GK5xRBl/ZxBA9B0/UR5avXOro github.com/google/trillian v1.7.3/go.mod h1:qh8iy4x/GvnVXUBd5pK4oncuT1Y9vVYfibQVsR/WpKg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.3.17 h1:73NfMHdiqo9JFU9+7a5ExpVa10/R29pXfZIaW559nrg= -github.com/googleapis/enterprise-certificate-proxy v0.3.17/go.mod h1:rSEsBUemEBZEexP2y6jPp16LUmUbjmSbcPMQizR0o4k= +github.com/googleapis/enterprise-certificate-proxy v0.3.18 h1:hvVi34VucdrV1IIsiWuqYM8kutw/92MxNEFxCJZEh0k= +github.com/googleapis/enterprise-certificate-proxy v0.3.18/go.mod h1:rSEsBUemEBZEexP2y6jPp16LUmUbjmSbcPMQizR0o4k= github.com/googleapis/gax-go/v2 v2.23.0 h1:Tchl7qkvE7Ip3y+ztvNufYFvkfqTe7NfLTYGIdJRLuE= github.com/googleapis/gax-go/v2 v2.23.0/go.mod h1:rBQKOVJCdb8IFEzg+FCwlt1LP/xMDGuqUXhUG+XMXEg= github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI= -github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 h1:B+8ClL/kCQkRiU82d9xajRPKYMrB7E0MbtzWVi1K4ns= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3/go.mod h1:NbCUVmiS4foBGBHOYlCT25+YmGpJ32dZPi75pGEUpj4= github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 h1:5VipnvEpbqr2gA2VbM+nYVbkIF28c5ZQfqCBQ5g2xfk= github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0/go.mod h1:Hyl3n6Twe1hvtd9XUXDec4pTvgMSEixRuQKPTMH2bNs= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= @@ -429,8 +428,8 @@ github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/hcl v1.0.1-vault-7 h1:ag5OxFVy3QYTFTJODRzTKVZ6xvdfLLCA1cy/Y6xGI0I= github.com/hashicorp/hcl v1.0.1-vault-7/go.mod h1:XYhtn6ijBSAj6n4YqAaf7RBPS4I06AItNorpy+MoQNM= -github.com/hashicorp/vault/api v1.22.0 h1:+HYFquE35/B74fHoIeXlZIP2YADVboaPjaSicHEZiH0= -github.com/hashicorp/vault/api v1.22.0/go.mod h1:IUZA2cDvr4Ok3+NtK2Oq/r+lJeXkeCrHRmqdyWfpmGM= +github.com/hashicorp/vault/api v1.23.0 h1:gXgluBsSECfRWTSW9niY2jwg2e9mMJc4WoHNv4g3h6A= +github.com/hashicorp/vault/api v1.23.0/go.mod h1:zransKiB9ftp+kgY8ydjnvCU7Wk8i9L0DYWpXeMj9ko= github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE= github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= github.com/howeyc/gopass v0.0.0-20210920133722-c8aef6fb66ef h1:A9HsByNhogrvm9cWb28sjiS3i7tcKCkflWFEkHfuAgM= @@ -488,8 +487,8 @@ github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOl github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jedisct1/go-minisign v0.0.0-20241212093149-d2f9f49435c7 h1:FWpSWRD8FbVkKQu8M1DM9jF5oXFLyE+XpisIYfdzbic= github.com/jedisct1/go-minisign v0.0.0-20241212093149-d2f9f49435c7/go.mod h1:BMxO138bOokdgt4UaxZiEfypcSHX0t6SIFimVP1oRfk= -github.com/jellydator/ttlcache/v3 v3.4.0 h1:YS4P125qQS0tNhtL6aeYkheEaB/m8HCqdMMP4mnWdTY= -github.com/jellydator/ttlcache/v3 v3.4.0/go.mod h1:Hw9EgjymziQD3yGsQdf1FqFdpp7YjFMd4Srg5EJlgD4= +github.com/jellydator/ttlcache/v3 v3.4.1 h1:bOdXmXiycyK6E6Qjyuj5vl+/vU3SCOoDs8a86NbHjAQ= +github.com/jellydator/ttlcache/v3 v3.4.1/go.mod h1:j7LO12PNghFg5+0v9budMAT4rDK4JY969jb9vOdOBBk= github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/joshdk/go-junit v1.0.0 h1:S86cUKIdwBHWwA6xCmFlf3RTLfVXYQfvanM5Uh+K6GE= @@ -638,8 +637,8 @@ github.com/nyaruka/phonenumbers v1.6.12 h1:aeGHjGQnfLhdN5/mZPevhoYMs13FWcQ0Vus0Y github.com/nyaruka/phonenumbers v1.6.12/go.mod h1:IUu45lj2bSeYXQuxDyyuzOrdV10tyRa1YSsfH8EKN5c= github.com/oauth2-proxy/mockoidc v0.0.0-20240214162133-caebfff84d25 h1:9bCMuD3TcnjeqjPT2gSlha4asp8NvgcFRYExCaikCxk= github.com/oauth2-proxy/mockoidc v0.0.0-20240214162133-caebfff84d25/go.mod h1:eDjgYHYDJbPLBLsyZ6qRaugP0mX8vePOhZ5id1fdzJw= -github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s= -github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= +github.com/oklog/ulid/v2 v2.1.2 h1:IEclFb9JNvzYA6MW2SCxbLzcHTVsfqm3PrqGQJH5zec= +github.com/oklog/ulid/v2 v2.1.2/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= github.com/oleiade/reflections v1.1.0 h1:D+I/UsXQB4esMathlt0kkZRJZdUDmhv5zGi/HOwYTWo= github.com/oleiade/reflections v1.1.0/go.mod h1:mCxx0QseeVCHs5Um5HhJeCKVC7AwS8kO67tky4rdisA= github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc= @@ -722,15 +721,13 @@ github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkB github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4= github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI= -github.com/sassoftware/relic v7.2.1+incompatible h1:Pwyh1F3I0r4clFJXkSI8bOyJINGqpgjJU3DYAZeI05A= -github.com/sassoftware/relic v7.2.1+incompatible/go.mod h1:CWfAxv73/iLZ17rbyhIEq3K9hs5w6FpNMdUT//qR+zk= -github.com/sassoftware/relic/v7 v7.6.2 h1:rS44Lbv9G9eXsukknS4mSjIAuuX+lMq/FnStgmZlUv4= -github.com/sassoftware/relic/v7 v7.6.2/go.mod h1:kjmP0IBVkJZ6gXeAu35/KCEfca//+PKM6vTAsyDPY+k= +github.com/sassoftware/relic/v8 v8.2.0 h1:9/L4S4I6an/JsPNhmcTpqfiOIsLb/iJaePZD1xR+ulE= +github.com/sassoftware/relic/v8 v8.2.0/go.mod h1:pZy7hLT9WCOKPonV8G/fplvtBLOZd6/kWtKqeHR6nKc= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/seatgeek/logrus-gelf-formatter v0.0.0-20210414080842-5b05eb8ff761 h1:0b8DF5kR0PhRoRXDiEEdzrgBc8UqVY4JWLkQJCRsLME= github.com/seatgeek/logrus-gelf-formatter v0.0.0-20210414080842-5b05eb8ff761/go.mod h1:/THDZYi7F/BsVEcYzYPqdcWFQ+1C2InkawTKfLOAnzg= -github.com/secure-systems-lab/go-securesystemslib v0.11.0 h1:iuCR9kcMFD4QurdKrGvPLoKZLv9YvwPYVr0473BdtFs= -github.com/secure-systems-lab/go-securesystemslib v0.11.0/go.mod h1:+PMOTjUGwHj2vcZ+TFKlb1tXRbrdWE1LYDT5i9JC80Q= +github.com/secure-systems-lab/go-securesystemslib v0.11.1 h1:ayahDPjfSIqKegyt5YVGEvQ7SAi72GHXTy+b2YjUd5w= +github.com/secure-systems-lab/go-securesystemslib v0.11.1/go.mod h1:UyOjhoZLi76ir63u1ptubEevVe6dlNPmNxwgVvKqFFY= github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0= github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= github.com/segmentio/encoding v0.5.4 h1:OW1VRern8Nw6ITAtwSZ7Idrl3MXCFwXHPgqESYfvNt0= @@ -746,24 +743,24 @@ github.com/shirou/gopsutil/v4 v4.26.7 h1:IXzpHz/dkMRYAhKkOXr1HB6SuzWU3eoyyeWe7g3 github.com/shirou/gopsutil/v4 v4.26.7/go.mod h1:5O9FjBiXoTDFatIWjZZosqj4pV0DRtLx598xGbBehzM= 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/sigstore/protobuf-specs v0.5.1 h1:/5OPaNuolRJmQfeZLayJGFXMpsRJEdgC6ah1/+7Px7U= -github.com/sigstore/protobuf-specs v0.5.1/go.mod h1:DRBzpFuE+LnvQMN10/dU6nBeKwVLGEQ6o2FovN2Rats= -github.com/sigstore/rekor v1.5.3 h1:0Tyolw3zreRgm7PUW8dccFLXGBThi08278jI8EXNSr4= -github.com/sigstore/rekor v1.5.3/go.mod h1:h3GK5dDqCcWJJZUJwdpKGSSmEV2GEjPUjJy3WTjBwzA= +github.com/sigstore/protobuf-specs v0.5.2 h1:RSWWUY8QrVTxbYH00jY/jg2e7YnjzrpwP+PeHTMll0E= +github.com/sigstore/protobuf-specs v0.5.2/go.mod h1:DRBzpFuE+LnvQMN10/dU6nBeKwVLGEQ6o2FovN2Rats= +github.com/sigstore/rekor v1.5.4 h1:A9zITTrkRwO+8lfq7L/gBR7rDHHcBQJRI+a0JQo4xDM= +github.com/sigstore/rekor v1.5.4/go.mod h1:0bcUHhZPlG+RVW1dI/7sdBp0MiVWVzk9h0tgOH/o0Yc= github.com/sigstore/rekor-tiles/v2 v2.3.0 h1:HhMgH61UP0t899V8Fjt7pz1YdgOBptbaQdnCF+79cdc= github.com/sigstore/rekor-tiles/v2 v2.3.0/go.mod h1:DEFiKSyQ4nF75QRVNdOPaIH3cmvMkO2B6xDZjNYngPc= github.com/sigstore/sigstore v1.10.9 h1:7Dcpt+ibnltHQZ8XhaU0dFmhHaf/T491eJfA9WDex4Y= github.com/sigstore/sigstore v1.10.9/go.mod h1:LYW9+qH7bK8wZmLm6lPxIC5lkHtkJDCgkqjChzTAIBs= github.com/sigstore/sigstore-go v1.3.0 h1:hnIMHREyCNTYFtOE1o7ae3Axa9B5W5EjUSBJICP2NBE= github.com/sigstore/sigstore-go v1.3.0/go.mod h1:AyRQXfpH89py1twjE3kEZxlRersng90GSYqQV9zGJE8= -github.com/sigstore/sigstore/pkg/signature/kms/aws v1.10.8 h1:tofVQ+UWJgad/69I5zbqxdFCN5gpIn9tRQP7iBzIpBw= -github.com/sigstore/sigstore/pkg/signature/kms/aws v1.10.8/go.mod h1:73AfJE8H6w5KGCFPBu4x/OG+i1Yxgmh0L/FtV7prd88= -github.com/sigstore/sigstore/pkg/signature/kms/azure v1.10.8 h1:8Mt7J36GcUEmbiJaiFhz2tud5ZIgkfVVCe2H/WJCHmw= -github.com/sigstore/sigstore/pkg/signature/kms/azure v1.10.8/go.mod h1:YiTpAsxoWXhF9KlLOVWCh7BckN5cYO8X01WufDq1ido= -github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.10.8 h1:MxpAIMZVzn0Tpbarc9ax1I498oQBp7oYSMgoMSsOmKI= -github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.10.8/go.mod h1:bnAUEkFNam6STvkVZhptVwWzWR5pS24CEtQ+lhxu7S0= -github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.10.8 h1:1DGe4/clcdOnkz5MINEczWlmEvjUtZd+AjPPT/cBhQ8= -github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.10.8/go.mod h1:6IDFhpgxtzqbnzrFkyegbj7RfWwKeRrb3/+xAD1Wp+Y= +github.com/sigstore/sigstore/pkg/signature/kms/aws v1.10.9 h1:2NWAU/utJRwzy1OrQurlY3wlDSynyH1rNJS5GgFchrg= +github.com/sigstore/sigstore/pkg/signature/kms/aws v1.10.9/go.mod h1:2shgYiPHTzeb5NNEu6okco+LOojMS5n9p/5Wdp0jhGI= +github.com/sigstore/sigstore/pkg/signature/kms/azure v1.10.9 h1:zcg/EG6JW/8RSx9T2g5j6zqxplaE4dymmhiFflDE6ks= +github.com/sigstore/sigstore/pkg/signature/kms/azure v1.10.9/go.mod h1:e7QngtHib0McuP1C/+Dt9f/7a8ic8FXvPB75OQzWgU4= +github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.10.9 h1:DG8R4orWDs3l9trp70fLRTgazedEDoAIo/cHEN7Qo0k= +github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.10.9/go.mod h1:d6oIwQ0YiYHMcFO4xqFcAx5njXwIf0dm5KfdytOar8A= +github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.10.9 h1:V+spyeSHypvLHSOvPMT+V72dxDlS0LDkDd6lwCsUOXM= +github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.10.9/go.mod h1:o7u5bQNLqpDQFaST29dpo6P6ihMV+wicwqfMmMW6H3c= github.com/sigstore/timestamp-authority/v2 v2.1.3 h1:Fc+LjCTfik1lh3YLkaosENfkXa3R2Y1nswiUKutBdFA= github.com/sigstore/timestamp-authority/v2 v2.1.3/go.mod h1:myoFOKJB/u5vNTFwvBBJVkG3NnOBeIJevbfjNeasLjo= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= @@ -791,8 +788,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.20260817.0 h1:DeGLHpBzysPAIW4UL3jlKklW8WEf9EBCHXNk60VvaRc= github.com/stacklok/toolhive-catalog v0.20260817.0/go.mod h1:UOTFxQsGRrnKX8pOZjzkTcSIslCdAzgpj3P5ZT4oeQg= -github.com/stacklok/toolhive-core v0.0.41 h1:Yf+uvKra2IMZTiU9Cy8BcZNqDimcxMnfKlYbiHN0kWU= -github.com/stacklok/toolhive-core v0.0.41/go.mod h1:N0LRksZqJoTWKVyRc3xP4VdFLsctivTkVeHutiJ8e/k= +github.com/stacklok/toolhive-core v0.0.42 h1:FqE5CfBQrSrbYyHarqsK6K0GDy1jHKwPAhgp9mwfujk= +github.com/stacklok/toolhive-core v0.0.42/go.mod h1:HzVEFVqDt6PqSN9C1dKocuq54dvfYjNaWtRxD/+B4Ck= 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= @@ -808,8 +805,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= +github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/sv-tools/openapi v0.4.0 h1:UhD9DVnGox1hfTePNclpUzUFgos57FvzT2jmcAuTOJ4= @@ -839,8 +836,8 @@ github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/tink-crypto/tink-go-awskms/v3 v3.0.0 h1:XSohRhCkXAVI0iaCnWB/GS05TEmpnKurQmzaY1jzt3Y= github.com/tink-crypto/tink-go-awskms/v3 v3.0.0/go.mod h1:+7MXsShLzVbSQ6dI0Pe4JuZM52jD1jQ1itAygd/MDsA= -github.com/tink-crypto/tink-go-gcpkms/v2 v2.3.0 h1:3s6YMgMOBZRU8qG6ybpKSF2Sau+y3sMvxR911M59SwA= -github.com/tink-crypto/tink-go-gcpkms/v2 v2.3.0/go.mod h1:X8UNvbQu2wanAGa8ixRUU/DWt1V2hUBfvPGy6s9nE2s= +github.com/tink-crypto/tink-go-gcpkms/v2 v2.4.0 h1:xpI5pnEQ0erFCS3JmcEl7Blluo8ZieEPNOURqy9uD70= +github.com/tink-crypto/tink-go-gcpkms/v2 v2.4.0/go.mod h1:wDDAhjfd1t4TjeJCSsmFK7CARPUs/ITi16ZZ8kmAaFQ= github.com/tink-crypto/tink-go-hcvault/v2 v2.5.0 h1:eXuNqgrcYelxU1MVikOJDP3wTS5lvihM4ntoAbAMfvs= github.com/tink-crypto/tink-go-hcvault/v2 v2.5.0/go.mod h1:3RhcxAqek6xUlRFmJifvU4CYLZN60KMQdIKqpZAZJG0= github.com/tink-crypto/tink-go/v2 v2.7.0 h1:k7QnUXJ1cRDpvoy/5l1FimZqMAArRff8vjUqzi5N04o= @@ -915,6 +912,8 @@ go.opentelemetry.io/otel v1.45.0 h1:pdrWmLHofpubmArBv1LgFSv1Z0Ie/ppdZzu+kUN5EeU= go.opentelemetry.io/otel v1.45.0/go.mod h1:XZxIqPapzEYnhNSScF5DIqXhm/rYi0FzCe2XddAwZfQ= go.opentelemetry.io/otel/exporters/jaeger v1.17.0 h1:D7UpUy2Xc2wsi1Ras6V40q806WM07rqoCWzXu7Sqy+4= go.opentelemetry.io/otel/exporters/jaeger v1.17.0/go.mod h1:nPCqOnEH9rNLKqH/+rrUjiMzHJdV1BlpKcTwRTyKkKI= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.21.0 h1:fvNHGyo3CdRv/DQveXqhqBxnKTDyRaC5sMSQxilX/A0= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.21.0/go.mod h1:zyGrjRKL2B/6+Jc/m4/otPoZqV2MY9ZjC/aBraRO7zc= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.45.0 h1:pnxy6c/kvNBWdNNFzqpjuJLm9Hjhgk/Q0nY221rwuk0= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.45.0/go.mod h1:qw6YsFapotRwoDhXRZvljzaOvCQB7UfnafEJagpN2TA= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.45.0 h1:QRefszxJmfPdjXUUm3j6iDzY03mTPXMjqErFqQ67vUg= @@ -925,12 +924,18 @@ go.opentelemetry.io/otel/exporters/prometheus v0.67.0 h1:7IefDa35e6V3NoiqIeLDMDx go.opentelemetry.io/otel/exporters/prometheus v0.67.0/go.mod h1:nsPI1awTg5Vmg1YrommL2mVarVGlqc4yXOoKAkPRD0c= go.opentelemetry.io/otel/exporters/zipkin v1.21.0 h1:D+Gv6lSfrFBWmQYyxKjDd0Zuld9SRXpIrEsKZvE4DO4= go.opentelemetry.io/otel/exporters/zipkin v1.21.0/go.mod h1:83oMKR6DzmHisFOW3I+yIMGZUTjxiWaiBI8M8+TU5zE= +go.opentelemetry.io/otel/log v0.21.0 h1:SLsVDGmtyBrdw8/a2Z0bOIxou/+bN4z56GebH7T0LvA= +go.opentelemetry.io/otel/log v0.21.0/go.mod h1:iReetQrZL9Wyg84cCkOoCmqDHS5RCFfyxC7J+r8fn8g= go.opentelemetry.io/otel/metric v1.45.0 h1:7Eg1uH7CJ5cXv9is6tnBe1FI6rj1nwUdbFypRm3br/M= go.opentelemetry.io/otel/metric v1.45.0/go.mod h1:HAPbm1nd3p1PmFH7v2dR+6BjXxw+Lq4a2+pndMAm08s= go.opentelemetry.io/otel/metric/x v0.67.0 h1:PcicCNZFkZ4bXfSooXdo3WN7RBOVOtjVdo1wD358Uns= go.opentelemetry.io/otel/metric/x v0.67.0/go.mod h1:FBjCWZe6wgcqxcMtjdGiClDKXb2YxxXii0CXftE4QtI= go.opentelemetry.io/otel/sdk v1.45.0 h1:4VVSMgQ83dUgW2aoX5f6JgLvHwIvzcuLnF9lUdCSpCw= go.opentelemetry.io/otel/sdk v1.45.0/go.mod h1:Sr40LgXV7DsKMMJMKOhUWOgMWTfAaqvm2kF0g7ilwuA= +go.opentelemetry.io/otel/sdk/log v0.21.0 h1:QsE7XSR0ktQdKmRKGnR+f1ObGF32WG+7MER/P9KgmYc= +go.opentelemetry.io/otel/sdk/log v0.21.0/go.mod h1:m9mApjCoD2/1QuKCAptjv+BrG9WKOvQLVdNx+iBldTo= +go.opentelemetry.io/otel/sdk/log/logtest v0.21.0 h1:X+JBBgKlswCGYsmgL0CnoUUtlE//VB345c84jYAYkdQ= +go.opentelemetry.io/otel/sdk/log/logtest v0.21.0/go.mod h1:HD1575K8e6sIFBBDd5tZB3t9DlMytWXq9FuR+Y4rfjE= go.opentelemetry.io/otel/sdk/metric v1.45.0 h1:oVFszMfyj1Am6s24Vtc7wBb8BKLcwepJjNEYILuiE3o= go.opentelemetry.io/otel/sdk/metric v1.45.0/go.mod h1:vUWUxDZvu1WVRj8JA8S0AdhsPrZoDpA2DdZauIh4mDA= go.opentelemetry.io/otel/trace v1.45.0 h1:l/mP6Uv7oNO7/TblbhpbgMidxhq1uO/rPsikOyVhxag= @@ -939,8 +944,8 @@ go.opentelemetry.io/proto/otlp v1.11.0 h1:5rrYs0Ykyj50sdU/JU0x8etU+LubXWb+gED6Tb go.opentelemetry.io/proto/otlp v1.11.0/go.mod h1:SmVizdCOAm3XBtG1g1NnOdhW6jtddT72hLMhv8VwA8E= go.starlark.net v0.0.0-20260630144053-529d8e869a14 h1:w4krPU6GwY87DdwH30fr/GygfD3ROuoBq3YWilzFe6A= go.starlark.net v0.0.0-20260630144053-529d8e869a14/go.mod h1:Iue6g6iirlfLoVi/DYCi5/x0h/bAOuWF3dULTKpt2Vo= -go.step.sm/crypto v0.77.7 h1:6azC+pD678Vjju8yXnMDHCZJ+HzFaEmL3sCryiezTIA= -go.step.sm/crypto v0.77.7/go.mod h1:OW/2sEHwTtDKq70PvSQ5B0JGy/CrLyDKOiVy3YvZMTQ= +go.step.sm/crypto v0.87.0 h1:+u0pDz1OV9M/SswA6KtcQ3GSrYMMelbL//0W/ceJVYI= +go.step.sm/crypto v0.87.0/go.mod h1:gBr1mpMiKs5804/Yt03uAoOBiw3DFxARZnGUKEUO82M= 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= @@ -964,8 +969,9 @@ go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo= go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q= go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= -go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= +go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= golang.ngrok.com/muxado/v2 v2.0.1 h1:jM9i6Pom6GGmnPrHKNR6OJRrUoHFkSZlJ3/S0zqdVpY= golang.ngrok.com/muxado/v2 v2.0.1/go.mod h1:wzxJYX4xiAtmwumzL+QsukVwFRXmPNv86vB8RPpOxyM= golang.ngrok.com/ngrok/v2 v2.1.4 h1:0JQZRqzVGBYluIi5MuhxNYx653qxpN7AiNwNJzoa9DQ= @@ -1117,18 +1123,18 @@ gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= -google.golang.org/api v0.287.1 h1:LiyJx32VU3cwQfLchn/513qKhc25hq0pEANYJoWNnnI= -google.golang.org/api v0.287.1/go.mod h1:lM2kYRzYUCBY91P9h6VF1PYmvhxii3O5hji37qRvIcY= -google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 h1:XzmzkmB14QhVhgnawEVsOn6OFsnpyxNPRY9QV01dNB0= -google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:L43LFes82YgSonw6iTXTxXUX1OlULt4AQtkik4ULL/I= +google.golang.org/api v0.290.0 h1:eMw0Xo+IfbbMlKmW7aHvpyQRv9RCXuWx/vs8AD+0x9A= +google.golang.org/api v0.290.0/go.mod h1:weJZ3lldHFYI0DBFNKpJelUDNnusTt5YaOEgxvt8ci8= +google.golang.org/genproto v0.0.0-20260622175928-b703f567277d h1:CP5omUq8AJTiWMrPKM1WRLJ7zZeXd9OPcQD3TbBNAyY= +google.golang.org/genproto v0.0.0-20260622175928-b703f567277d/go.mod h1:DrwuGJgFSEVNpv3S5Q5VxhRTvdnjauw9GtvwVOEARfA= google.golang.org/genproto/googleapis/api v0.0.0-20260803160001-6ac0973c030d h1:FarXi840EJWSHYTN3ERkADbPWjl307+FGrA22KAVjjc= google.golang.org/genproto/googleapis/api v0.0.0-20260803160001-6ac0973c030d/go.mod h1:K/+WGbmBY7aNW1HDw1fJnKYo10i0DkAX6pows00dLig= google.golang.org/genproto/googleapis/rpc v0.0.0-20260803160001-6ac0973c030d h1:IL4hdHzcUv2l/gcg98/Rj3FbtE6axwqslOW8SW0C+S0= google.golang.org/genproto/googleapis/rpc v0.0.0-20260803160001-6ac0973c030d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.83.0 h1:JeNZEKJFbQxArAMl+hiytHauacDNqJUllNfmIMmpqnQ= google.golang.org/grpc v1.83.0/go.mod h1:kDyl6SKsiHKt0uylY5gtn5cEjkrIOhQOGDgIc4JGwzQ= -google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= -google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc= +google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -1207,5 +1213,5 @@ sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 h1:2WO sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= -software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k= -software.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= +software.sslmate.com/src/go-pkcs12 v0.5.0 h1:EC6R394xgENTpZ4RltKydeDUjtlM5drOYIG9c6TVj2M= +software.sslmate.com/src/go-pkcs12 v0.5.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= diff --git a/pkg/auth/awssts/role_mapper.go b/pkg/auth/awssts/role_mapper.go index 0fbad5ef80..d030d6a7e8 100644 --- a/pkg/auth/awssts/role_mapper.go +++ b/pkg/auth/awssts/role_mapper.go @@ -11,8 +11,8 @@ import ( "slices" "strings" + celgo "cel.dev/cel-go/cel" "github.com/aws/aws-sdk-go-v2/aws/arn" - celgo "github.com/google/cel-go/cel" "github.com/stacklok/toolhive-core/cel" ) diff --git a/pkg/authserver/server/tokenexchange/multi_issuer_validator.go b/pkg/authserver/server/tokenexchange/multi_issuer_validator.go index bcd99b270e..ac167eca59 100644 --- a/pkg/authserver/server/tokenexchange/multi_issuer_validator.go +++ b/pkg/authserver/server/tokenexchange/multi_issuer_validator.go @@ -18,9 +18,9 @@ import ( "sync" "time" + celgo "cel.dev/cel-go/cel" "github.com/go-jose/go-jose/v4" "github.com/go-jose/go-jose/v4/jwt" - celgo "github.com/google/cel-go/cel" "github.com/lestrrat-go/httprc/v3" "github.com/lestrrat-go/jwx/v3/jwk" From 2b396d8cd4f03ea227046ea4a639644b213c0c8e Mon Sep 17 00:00:00 2001 From: Samuele Verzi Date: Mon, 24 Aug 2026 16:41:31 +0200 Subject: [PATCH 2/6] Check first skill install against catalog-declared provenance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Trust-on-first-use had nothing to verify a skill's very first install against — the observed signer identity was simply recorded, never checked. When a skill resolves through the registry/catalog and that entry declares an expected provenance, use it as the first-install expectation instead of blind trust; a lock entry, once one exists, still always wins. A catalog-declared attestation constraint is refused rather than silently skipped: the skill verifier has no attestation-comparison logic today, so honoring identity while ignoring attestation would let a catalog author believe a guarantee is enforced when it isn't. Closes #6310 Signed-off-by: Samuele Verzi --- docs/arch/12-skills-system.md | 2 + pkg/skills/options.go | 8 + pkg/skills/skillsvc/install.go | 6 + pkg/skills/skillsvc/install_registry_test.go | 12 +- pkg/skills/skillsvc/registry.go | 20 ++- pkg/skills/skillsvc/verify.go | 50 +++++- pkg/skills/skillsvc/verify_test.go | 166 +++++++++++++++++++ 7 files changed, 249 insertions(+), 15 deletions(-) diff --git a/docs/arch/12-skills-system.md b/docs/arch/12-skills-system.md index 0a0cc8b121..a107ffc4f3 100644 --- a/docs/arch/12-skills-system.md +++ b/docs/arch/12-skills-system.md @@ -412,6 +412,8 @@ RFC [THV-0080](https://github.com/stacklok/toolhive-rfcs/blob/main/rfcs/THV-0080 **Trust model, stated plainly:** project-scoped installs are verified against Sigstore signatures, and the lock file records the trust decisions those verifications produce. On first install of a signed skill the observed signer identity is recorded (trust on first use) as the entry's `provenance:` block and **displayed to the user**; every later install, sync, and upgrade enforces that identity *inside* the Sigstore verification policy — OCI artifacts through their attached signature bundles, git commits through gitsign signature-and-chain verification (currently `provisional: true`: the transparency-log proof of signing time is not yet validated, so the replay window is unbounded until that lands). Sync additionally re-verifies each entry's stored signature bundle offline (embedded trust root, no network) before counting it current, and upgrade refuses to move to an artifact signed by a different identity — or unsigned — without an explicit `--allow-signer-change`. +**First install need not be blind.** When a skill resolves through the registry/catalog (a plain name like `thv skill install code-review`, not a direct OCI or git reference) and that catalog entry declares an expected `provenance` (`toolhive-core`'s `registry.Skill.Provenance`, RFC THV-0080 follow-up [#6310](https://github.com/stacklok/toolhive/issues/6310)), the very first install is checked against it instead of unconditionally trusting whatever the artifact's signature says. A lock entry, once one exists, always takes precedence over the catalog — the catalog only fills the gap before any trust decision has been recorded. An empty or absent catalog provenance is unconstrained, same as every other provenance field: most catalog entries won't declare one for a while, and that must not block installs. A catalog-declared attestation constraint is refused outright rather than silently skipped — the skill verifier (`pkg/skills/verifier`) has no attestation-comparison logic today, so honoring the identity fields while quietly ignoring `attestation` would let a catalog author believe a guarantee is enforced when it is not. + The Sigstore policy alone is not the whole guarantee: its SAN match deliberately leaves the signing workflow's git ref unpinned (`(@.*)?$`), so identity alone is satisfied by "the right workflow, on any branch." Two additional certificate fields — the git ref the signing workflow ran on and the runner class it executed in (`repositoryRef:`/`runnerEnvironment:` in `provenance:`) — are enforced separately, after the Sigstore policy succeeds, against the certificate's Fulcio extensions. An entry recorded before these fields existed, or a certificate that carries neither (a signer outside CI), is unconstrained on that field — never a wildcard match once something IS recorded. Install, sync, and upgrade all require the recorded ref and runner class to match exactly, with no automatic allowance for any kind of change, ref rotation included: an earlier version of this guard let a recorded tag ref rotate to any other tag ref automatically, reasoning that a release workflow signs each version on its own tag, but review found that this let a candidate signed from an attacker's own tag on the same repository (e.g. `refs/tags/attacker-release`) replace a pinned tag just as easily, since nothing tied the candidate's tag to the version actually being upgraded to. A ref or runner-class change of any shape is now blocked exactly like a genuine signer-identity change, and needs the same explicit `--allow-signer-change` to proceed and re-record it. What is still trusted on faith, deliberately and visibly: diff --git a/pkg/skills/options.go b/pkg/skills/options.go index 54cca8ecaa..c63a0e84b4 100644 --- a/pkg/skills/options.go +++ b/pkg/skills/options.go @@ -86,6 +86,14 @@ type InstallOptions struct { // install-time verification; recorded as `unsigned: true` in the lock // entry. Unsigned bool `json:"-"` + // CatalogProvenance is the expected signer identity declared by the + // skill registry/catalog entry this install resolved from, if any. When + // no lock entry already exists for this skill, install-time + // verification checks the artifact against this instead of blindly + // trusting whatever it observes (trust-on-first-use) — see RFC + // THV-0080 follow-up #6310. A lock entry, once one exists, always takes + // precedence over this. Internal use only — NOT exposed via HTTP API. + CatalogProvenance *ProvenanceInfo `json:"-"` // Provenance carries the verified signer identity established during // install-time verification, for recording into the lock entry. Set by // the verification step, nil when the artifact is unsigned or diff --git a/pkg/skills/skillsvc/install.go b/pkg/skills/skillsvc/install.go index f497e3540f..baf4ed251e 100644 --- a/pkg/skills/skillsvc/install.go +++ b/pkg/skills/skillsvc/install.go @@ -207,6 +207,12 @@ func (s *service) installFromResolvedRegistry( deps *depState, alreadyLocked bool, ) (*skills.InstallResult, error) { + catalogProvenance, provErr := provenanceInfoFromCatalog(resolved.Provenance) + if provErr != nil { + return nil, provErr + } + opts.CatalogProvenance = catalogProvenance + switch { case resolved.OCIRef != nil: slog.Info("resolved skill from registry (OCI)", "name", opts.Name, "oci_reference", resolved.OCIRef.String()) diff --git a/pkg/skills/skillsvc/install_registry_test.go b/pkg/skills/skillsvc/install_registry_test.go index db461f0268..c481a68553 100644 --- a/pkg/skills/skillsvc/install_registry_test.go +++ b/pkg/skills/skillsvc/install_registry_test.go @@ -391,11 +391,13 @@ func TestSplitQualifiedName(t *testing.T) { func TestResolveRegistryPackagesSubfolder(t *testing.T) { t.Parallel() - result, err := resolveRegistryPackages("my-skill", []regtypes.SkillPackage{ - { - RegistryType: "git", - URL: "https://github.com/org/repo", - Subfolder: "skills/my-skill", + result, err := resolveRegistryPackages("my-skill", regtypes.Skill{ + Packages: []regtypes.SkillPackage{ + { + RegistryType: "git", + URL: "https://github.com/org/repo", + Subfolder: "skills/my-skill", + }, }, }) require.NoError(t, err) diff --git a/pkg/skills/skillsvc/registry.go b/pkg/skills/skillsvc/registry.go index 5a7784eabf..4e5dd89d0b 100644 --- a/pkg/skills/skillsvc/registry.go +++ b/pkg/skills/skillsvc/registry.go @@ -21,6 +21,10 @@ import ( type registryResolveResult struct { OCIRef nameref.Reference GitURL string // raw git:// URL for installFromGit + // Provenance is the expected signer identity the catalog entry declares + // for this skill, if any (RFC THV-0080 follow-up #6310). nil when the + // entry declares none. + Provenance *regtypes.Provenance } // resolveFromRegistry attempts to resolve a skill name by querying the @@ -78,7 +82,7 @@ func (s *service) resolveFromRegistry(name string) (*registryResolveResult, erro ) } - return resolveRegistryPackages(name, matches[0].Packages) + return resolveRegistryPackages(name, matches[0]) } // splitQualifiedName splits "namespace/name" into (namespace, name). @@ -212,10 +216,12 @@ func preferredGitRef(pkg regtypes.SkillPackage) string { } // resolveRegistryPackages selects the best installable package from a registry -// entry. OCI packages are preferred; git is the fallback. -func resolveRegistryPackages(name string, packages []regtypes.SkillPackage) (*registryResolveResult, error) { +// entry. OCI packages are preferred; git is the fallback. The entry's +// declared provenance (if any) is carried onto the result regardless of +// which package type is chosen. +func resolveRegistryPackages(name string, sk regtypes.Skill) (*registryResolveResult, error) { // Try OCI packages first (preferred). - for _, pkg := range packages { + for _, pkg := range sk.Packages { if pkg.RegistryType == "oci" && pkg.Identifier != "" { ref, parseErr := nameref.ParseReference(pkg.Identifier) if parseErr != nil { @@ -225,12 +231,12 @@ func resolveRegistryPackages(name string, packages []regtypes.SkillPackage) (*re http.StatusUnprocessableEntity, ) } - return ®istryResolveResult{OCIRef: ref}, nil + return ®istryResolveResult{OCIRef: ref, Provenance: sk.Provenance}, nil } } // Fallback: look for git packages. - for _, pkg := range packages { + for _, pkg := range sk.Packages { if pkg.RegistryType == "git" && pkg.URL != "" { gitURL, gitErr := buildGitReferenceFromRegistryURL(pkg.URL) if gitErr != nil { @@ -243,7 +249,7 @@ func resolveRegistryPackages(name string, packages []regtypes.SkillPackage) (*re if pkg.Subfolder != "" { gitURL += "#" + pkg.Subfolder } - return ®istryResolveResult{GitURL: gitURL}, nil + return ®istryResolveResult{GitURL: gitURL, Provenance: sk.Provenance}, nil } } diff --git a/pkg/skills/skillsvc/verify.go b/pkg/skills/skillsvc/verify.go index d394b60208..067103a950 100644 --- a/pkg/skills/skillsvc/verify.go +++ b/pkg/skills/skillsvc/verify.go @@ -10,6 +10,7 @@ import ( "net/http" "github.com/stacklok/toolhive-core/httperr" + regtypes "github.com/stacklok/toolhive-core/registry/types" "github.com/stacklok/toolhive/pkg/container/images" "github.com/stacklok/toolhive/pkg/skills" "github.com/stacklok/toolhive/pkg/skills/lockfile" @@ -55,8 +56,11 @@ func applyDecisionToOpts(opts *skills.InstallOptions, decision *provenanceDecisi // verifyOCIInstall verifies the signature of the OCI artifact at ref/digest // before anything is extracted or recorded. The identity expected by the -// lock file (if any) is enforced inside the verifier's Sigstore policy; -// trust on first use records whatever identity verification observes. +// lock file (if any) is enforced inside the verifier's Sigstore policy; on +// true first use (no lock entry at all), a catalog-declared expectation +// takes its place if the install resolved from a registry entry that +// declared one (RFC THV-0080 follow-up #6310) — otherwise trust on first +// use records whatever identity verification observes. func (s *service) verifyOCIInstall( ctx context.Context, opts skills.InstallOptions, @@ -66,6 +70,9 @@ func (s *service) verifyOCIInstall( if err != nil { return nil, err } + if expected == nil && !expectUnsigned { + expected = provenanceInfoToLock(opts.CatalogProvenance) + } if opts.AllowSignerChange { // The signer-change guard was explicitly overridden: verify the // chain of trust only and re-record whatever identity is observed. @@ -89,7 +96,8 @@ func (s *service) verifyOCIInstall( } // verifyGitInstall verifies the gitsign signature on the resolved commit -// before anything is written or recorded. +// before anything is written or recorded. See verifyOCIInstall for the +// catalog-provenance fallback on true first use. func (s *service) verifyGitInstall( ctx context.Context, opts skills.InstallOptions, @@ -101,6 +109,9 @@ func (s *service) verifyGitInstall( if err != nil { return nil, err } + if expected == nil && !expectUnsigned { + expected = provenanceInfoToLock(opts.CatalogProvenance) + } if opts.AllowSignerChange { expected, expectUnsigned = nil, false } @@ -334,3 +345,36 @@ func provenanceInfoFromResult(r *verifier.Result) *skills.ProvenanceInfo { Provisional: r.Provisional, } } + +// provenanceInfoFromCatalog converts a skill registry/catalog entry's +// declared provenance into the internal shape used as the expected identity +// for first-install verification (RFC THV-0080 follow-up #6310). nil input +// yields nil output — most catalog entries won't declare one for a while, +// and that must not block installs. +// +// An attestation constraint is refused rather than silently dropped: +// toolhive's skill verifier (pkg/skills/verifier) has no attestation +// comparison logic today, so honoring the rest of the provenance while +// quietly ignoring Attestation would let a catalog author believe a +// guarantee is enforced when it is not — the same fail-closed principle +// toolhive-core's own Skill.Validate applies to catalog authors. +func provenanceInfoFromCatalog(p *regtypes.Provenance) (*skills.ProvenanceInfo, error) { + if p == nil { + return nil, nil + } + if p.Attestation != nil { + return nil, httperr.WithCode( + errors.New("catalog declares an attestation constraint for this skill,"+ + " which toolhive cannot yet enforce; refusing to silently ignore it"), + http.StatusUnprocessableEntity, + ) + } + return &skills.ProvenanceInfo{ + SignerIdentity: p.SignerIdentity, + CertIssuer: p.CertIssuer, + RepositoryURI: p.RepositoryURI, + RepositoryRef: p.RepositoryRef, + RunnerEnvironment: p.RunnerEnvironment, + SigstoreURL: p.SigstoreURL, + }, nil +} diff --git a/pkg/skills/skillsvc/verify_test.go b/pkg/skills/skillsvc/verify_test.go index 37073f9461..dea185e79d 100644 --- a/pkg/skills/skillsvc/verify_test.go +++ b/pkg/skills/skillsvc/verify_test.go @@ -15,7 +15,11 @@ import ( "go.uber.org/mock/gomock" "github.com/stacklok/toolhive-core/httperr" + regtypes "github.com/stacklok/toolhive-core/registry/types" + regmocks "github.com/stacklok/toolhive/pkg/registry/mocks" "github.com/stacklok/toolhive/pkg/skills" + "github.com/stacklok/toolhive/pkg/skills/gitresolver" + gitmocks "github.com/stacklok/toolhive/pkg/skills/gitresolver/mocks" "github.com/stacklok/toolhive/pkg/skills/lockfile" "github.com/stacklok/toolhive/pkg/skills/verifier" verifiermocks "github.com/stacklok/toolhive/pkg/skills/verifier/mocks" @@ -104,6 +108,123 @@ func TestInstallVerification_TOFURecordsProvenance(t *testing.T) { require.NoError(t, err) } +// catalogSkill returns a registry/catalog entry for "catalog-skill" resolving +// to a git package, declaring the given provenance (RFC THV-0080 follow-up +// #6310). +func catalogSkill(provenance *regtypes.Provenance) regtypes.Skill { + return regtypes.Skill{ + Namespace: "io.github.test", + Name: "catalog-skill", + Packages: []regtypes.SkillPackage{ + {RegistryType: "git", URL: "https://github.com/test/catalog-skill"}, + }, + Provenance: provenance, + } +} + +// catalogSkillFiles is the git-resolved content installFromGit expects for +// the catalogSkill fixture. +func catalogSkillFiles() *gitresolver.ResolveResult { + return &gitresolver.ResolveResult{ + SkillConfig: &skills.ParseResult{Name: "catalog-skill", Version: "1.0.0"}, + Files: []gitresolver.FileEntry{ + {Path: "SKILL.md", Content: gitSkill("catalog-skill"), Mode: 0o644}, + }, + CommitHash: testCommitHash, + } +} + +//nolint:paralleltest // uses t.Setenv via newLockTestService, incompatible with t.Parallel +func TestInstallVerification_CatalogProvenanceUsedOnFirstUse(t *testing.T) { + gr := gitmocks.NewMockResolver(gomock.NewController(t)) + gr.EXPECT().Resolve(gomock.Any(), gomock.Any()).Return(catalogSkillFiles(), nil) + + lookup := regmocks.NewMockProvider(gomock.NewController(t)) + lookup.EXPECT().SearchSkills("catalog-skill").Return([]regtypes.Skill{ + catalogSkill(®types.Provenance{ + SignerIdentity: testSignerIdentity, + CertIssuer: testCertIssuer, + }), + }, nil) + + mv := verifiermocks.NewMockVerifier(gomock.NewController(t)) + // No lock entry exists yet, but the registry entry declares an expected + // identity: it must reach the verifier instead of nil (blind TOFU). + mv.EXPECT().VerifyGit(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(_ any, _, _ []byte, expected *lockfile.Provenance) (*verifier.Result, error) { + require.NotNil(t, expected, "a catalog-declared provenance must be checked on first install") + assert.Equal(t, testSignerIdentity, expected.SignerIdentity) + assert.Equal(t, testCertIssuer, expected.CertIssuer) + return signedResult(), nil + }) + + svc, projectRoot := newLockTestService(t, gr, WithVerifier(mv), WithSkillLookup(lookup)) + _, err := svc.Install(t.Context(), skills.InstallOptions{ + Name: "catalog-skill", + Scope: skills.ScopeProject, + ProjectRoot: projectRoot, + Clients: []string{"claude-code"}, + }) + require.NoError(t, err) + + entry, ok := loadLockEntry(t, projectRoot, "catalog-skill") + require.True(t, ok) + assert.Equal(t, testSignerIdentity, entry.Provenance.SignerIdentity) +} + +//nolint:paralleltest // uses t.Setenv via newLockTestService, incompatible with t.Parallel +func TestInstallVerification_LockEntryTakesPrecedenceOverCatalog(t *testing.T) { + gr := gitmocks.NewMockResolver(gomock.NewController(t)) + gr.EXPECT().Resolve(gomock.Any(), gomock.Any()).Return(catalogSkillFiles(), nil).AnyTimes() + + lookup := regmocks.NewMockProvider(gomock.NewController(t)) + // The catalog now declares a DIFFERENT identity than what will end up + // locked — if the lock entry didn't win, this install would enforce the + // wrong expectation. + lookup.EXPECT().SearchSkills("catalog-skill").Return([]regtypes.Skill{ + catalogSkill(®types.Provenance{SignerIdentity: "/.github/workflows/other.yml", CertIssuer: testCertIssuer}), + }, nil).AnyTimes() + + mv := verifiermocks.NewMockVerifier(gomock.NewController(t)) + // First install: no lock entry yet, so the catalog identity is checked + // and — since it verifies — becomes what gets locked. + mv.EXPECT().VerifyGit(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + Return(&verifier.Result{ + Signed: true, SignerIdentity: "/.github/workflows/other.yml", CertIssuer: testCertIssuer, + SigstoreURL: "https://rekor.sigstore.dev", Bundle: []byte(`{"bundle":true}`), + }, nil) + + svc, projectRoot := newLockTestService(t, gr, WithVerifier(mv), WithSkillLookup(lookup)) + _, err := svc.Install(t.Context(), skills.InstallOptions{ + Name: "catalog-skill", + Scope: skills.ScopeProject, + ProjectRoot: projectRoot, + Clients: []string{"claude-code"}, + }) + require.NoError(t, err) + + // Reinstall: the lock entry now recorded is what must be enforced, not + // the catalog's declared identity. + mv.EXPECT().VerifyGit(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(_ any, _, _ []byte, expected *lockfile.Provenance) (*verifier.Result, error) { + require.NotNil(t, expected) + assert.Equal(t, "/.github/workflows/other.yml", expected.SignerIdentity, + "the lock entry, not the catalog, must be enforced once one exists") + return &verifier.Result{ + Signed: true, SignerIdentity: "/.github/workflows/other.yml", CertIssuer: testCertIssuer, + SigstoreURL: "https://rekor.sigstore.dev", Bundle: []byte(`{"bundle":true}`), + }, nil + }) + _, err = svc.Install(t.Context(), skills.InstallOptions{ + Name: "catalog-skill", + Scope: skills.ScopeProject, + ProjectRoot: projectRoot, + Clients: []string{"claude-code"}, + Force: true, + }) + require.NoError(t, err) +} + //nolint:paralleltest // uses t.Setenv via newLockTestService, incompatible with t.Parallel func TestInstallVerification_UnsignedRejectedWithoutFlag(t *testing.T) { gr, fixtures := newGitResolverMock(t) @@ -393,6 +514,51 @@ func TestProvenanceConversionsPreserveEveryField(t *testing.T) { assert.Nil(t, provenanceInfoToLock(nil)) } +func TestProvenanceInfoFromCatalog(t *testing.T) { + t.Parallel() + + t.Run("nil is nil", func(t *testing.T) { + t.Parallel() + info, err := provenanceInfoFromCatalog(nil) + require.NoError(t, err) + assert.Nil(t, info) + }) + + t.Run("every identity field carries through", func(t *testing.T) { + t.Parallel() + catalog := ®types.Provenance{ + SignerIdentity: testSignerIdentity, + CertIssuer: testCertIssuer, + RepositoryURI: "https://github.com/org/repo", + RepositoryRef: "refs/heads/main", + RunnerEnvironment: testRunnerEnvironment, + SigstoreURL: "https://rekor.sigstore.dev", + } + info, err := provenanceInfoFromCatalog(catalog) + require.NoError(t, err) + require.NotNil(t, info) + assert.Equal(t, catalog.SignerIdentity, info.SignerIdentity) + assert.Equal(t, catalog.CertIssuer, info.CertIssuer) + assert.Equal(t, catalog.RepositoryURI, info.RepositoryURI) + assert.Equal(t, catalog.RepositoryRef, info.RepositoryRef) + assert.Equal(t, catalog.RunnerEnvironment, info.RunnerEnvironment) + assert.Equal(t, catalog.SigstoreURL, info.SigstoreURL) + assert.False(t, info.Provisional, "a catalog-declared expectation is not itself a verified result") + }) + + t.Run("attestation constraint is refused, not silently dropped", func(t *testing.T) { + t.Parallel() + catalog := ®types.Provenance{ + SignerIdentity: testSignerIdentity, + Attestation: ®types.VerifiedAttestation{PredicateType: "https://slsa.dev/provenance/v1"}, + } + _, err := provenanceInfoFromCatalog(catalog) + require.Error(t, err) + assert.Equal(t, http.StatusUnprocessableEntity, httperr.Code(err)) + assert.Contains(t, err.Error(), "attestation") + }) +} + // requireAllFieldsSet fails when any field of the struct pointed to by v holds // its zero value, so a field added to one provenance shape without a matching // line in the conversions is caught here rather than in production. From 0fddaddf19cceb465d534982f816e491e5dbd0f3 Mon Sep 17 00:00:00 2001 From: Samuele Verzi Date: Mon, 24 Aug 2026 17:01:47 +0200 Subject: [PATCH 3/6] Regenerate swagger docs for Skill.provenance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit toolhive-core's Skill type gained a Provenance field in the v0.0.42 bump, and it's exposed through the registry API — task docs was missed for this in the prior commit. Signed-off-by: Samuele Verzi --- docs/server/docs.go | 5 ++++- docs/server/swagger.json | 5 ++++- docs/server/swagger.yaml | 17 ++++++++++++++++- 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/docs/server/docs.go b/docs/server/docs.go index 180b90a904..0691824859 100644 --- a/docs/server/docs.go +++ b/docs/server/docs.go @@ -4643,7 +4643,7 @@ const docTemplate = `{ "type": "object" }, "registry.Provenance": { - "description": "Provenance contains verification and signing metadata", + "description": "Provenance is the expected signer identity for this skill, checked on\nfirst install instead of trust-on-first-use. Absent means unconstrained\n— most catalog entries won't have this for a while, and that must not\nbreak installs; it's an opt-in tightening per entry, not a requirement.\n\nEach field constrains independently, and an empty string leaves that\ndimension unconstrained. Attestation is the exception: setting it at\nall, even to an empty struct, requires the artifact to be attested, so\nverification fails against a signature carrying no statement. Its own\nPredicateType and Predicate then follow the usual rule and constrain\nonly when set. Predicate must be a JSON object; anything else can never\nmatch, and Validate rejects it rather than letting it through as a\nconstraint that silently fails every artifact.", "properties": { "attestation": { "$ref": "#/components/schemas/registry.VerifiedAttestation" @@ -4854,6 +4854,9 @@ const docTemplate = `{ "type": "array", "uniqueItems": false }, + "provenance": { + "$ref": "#/components/schemas/registry.Provenance" + }, "repository": { "$ref": "#/components/schemas/registry.SkillRepository" }, diff --git a/docs/server/swagger.json b/docs/server/swagger.json index 8555fddda8..4e2584eec6 100644 --- a/docs/server/swagger.json +++ b/docs/server/swagger.json @@ -4636,7 +4636,7 @@ "type": "object" }, "registry.Provenance": { - "description": "Provenance contains verification and signing metadata", + "description": "Provenance is the expected signer identity for this skill, checked on\nfirst install instead of trust-on-first-use. Absent means unconstrained\n— most catalog entries won't have this for a while, and that must not\nbreak installs; it's an opt-in tightening per entry, not a requirement.\n\nEach field constrains independently, and an empty string leaves that\ndimension unconstrained. Attestation is the exception: setting it at\nall, even to an empty struct, requires the artifact to be attested, so\nverification fails against a signature carrying no statement. Its own\nPredicateType and Predicate then follow the usual rule and constrain\nonly when set. Predicate must be a JSON object; anything else can never\nmatch, and Validate rejects it rather than letting it through as a\nconstraint that silently fails every artifact.", "properties": { "attestation": { "$ref": "#/components/schemas/registry.VerifiedAttestation" @@ -4847,6 +4847,9 @@ "type": "array", "uniqueItems": false }, + "provenance": { + "$ref": "#/components/schemas/registry.Provenance" + }, "repository": { "$ref": "#/components/schemas/registry.SkillRepository" }, diff --git a/docs/server/swagger.yaml b/docs/server/swagger.yaml index 7ff2940444..0decdc8565 100644 --- a/docs/server/swagger.yaml +++ b/docs/server/swagger.yaml @@ -4284,7 +4284,20 @@ components: type: string type: object registry.Provenance: - description: Provenance contains verification and signing metadata + description: |- + Provenance is the expected signer identity for this skill, checked on + first install instead of trust-on-first-use. Absent means unconstrained + — most catalog entries won't have this for a while, and that must not + break installs; it's an opt-in tightening per entry, not a requirement. + + Each field constrains independently, and an empty string leaves that + dimension unconstrained. Attestation is the exception: setting it at + all, even to an empty struct, requires the artifact to be attested, so + verification fails against a signature carrying no statement. Its own + PredicateType and Predicate then follow the usual rule and constrain + only when set. Predicate must be a JSON object; anything else can never + match, and Validate rejects it rather than letting it through as a + constraint that silently fails every artifact. properties: attestation: $ref: '#/components/schemas/registry.VerifiedAttestation' @@ -4474,6 +4487,8 @@ components: $ref: '#/components/schemas/registry.SkillPackage' type: array uniqueItems: false + provenance: + $ref: '#/components/schemas/registry.Provenance' repository: $ref: '#/components/schemas/registry.SkillRepository' status: From 52b4c3e2ab77b4013f49acc6aa673f909a879a87 Mon Sep 17 00:00:00 2001 From: Samuele Verzi Date: Mon, 24 Aug 2026 19:23:27 +0200 Subject: [PATCH 4/6] Fix catalog provenance verification Signed-off-by: Samuele Verzi --- docs/arch/12-skills-system.md | 4 +- pkg/skills/options.go | 17 +- pkg/skills/skillsvc/install.go | 10 +- pkg/skills/skillsvc/list.go | 2 +- pkg/skills/skillsvc/upgrade_verify_test.go | 13 +- pkg/skills/skillsvc/verify.go | 126 +++-- pkg/skills/skillsvc/verify_test.go | 537 ++++++++++++++++++--- pkg/skills/verifier/git.go | 12 +- pkg/skills/verifier/git_test.go | 9 +- pkg/skills/verifier/mocks/mock_verifier.go | 4 +- pkg/skills/verifier/oci.go | 74 +-- pkg/skills/verifier/offline.go | 4 +- pkg/skills/verifier/types.go | 95 +++- pkg/skills/verifier/verifier.go | 25 +- pkg/skills/verifier/verifier_test.go | 103 +++- 15 files changed, 813 insertions(+), 222 deletions(-) diff --git a/docs/arch/12-skills-system.md b/docs/arch/12-skills-system.md index a107ffc4f3..5678877b21 100644 --- a/docs/arch/12-skills-system.md +++ b/docs/arch/12-skills-system.md @@ -412,7 +412,9 @@ RFC [THV-0080](https://github.com/stacklok/toolhive-rfcs/blob/main/rfcs/THV-0080 **Trust model, stated plainly:** project-scoped installs are verified against Sigstore signatures, and the lock file records the trust decisions those verifications produce. On first install of a signed skill the observed signer identity is recorded (trust on first use) as the entry's `provenance:` block and **displayed to the user**; every later install, sync, and upgrade enforces that identity *inside* the Sigstore verification policy — OCI artifacts through their attached signature bundles, git commits through gitsign signature-and-chain verification (currently `provisional: true`: the transparency-log proof of signing time is not yet validated, so the replay window is unbounded until that lands). Sync additionally re-verifies each entry's stored signature bundle offline (embedded trust root, no network) before counting it current, and upgrade refuses to move to an artifact signed by a different identity — or unsigned — without an explicit `--allow-signer-change`. -**First install need not be blind.** When a skill resolves through the registry/catalog (a plain name like `thv skill install code-review`, not a direct OCI or git reference) and that catalog entry declares an expected `provenance` (`toolhive-core`'s `registry.Skill.Provenance`, RFC THV-0080 follow-up [#6310](https://github.com/stacklok/toolhive/issues/6310)), the very first install is checked against it instead of unconditionally trusting whatever the artifact's signature says. A lock entry, once one exists, always takes precedence over the catalog — the catalog only fills the gap before any trust decision has been recorded. An empty or absent catalog provenance is unconstrained, same as every other provenance field: most catalog entries won't declare one for a while, and that must not block installs. A catalog-declared attestation constraint is refused outright rather than silently skipped — the skill verifier (`pkg/skills/verifier`) has no attestation-comparison logic today, so honoring the identity fields while quietly ignoring `attestation` would let a catalog author believe a guarantee is enforced when it is not. +**First install need not be blind.** When a skill resolves through the registry/catalog (a plain name like `thv skill install code-review`, not a direct OCI or git reference) and that catalog entry declares an expected `provenance` (`toolhive-core`'s `registry.Skill.Provenance`, RFC THV-0080 follow-up [#6310](https://github.com/stacklok/toolhive/issues/6310)), the very first project-scoped install is checked against it instead of unconditionally trusting whatever the artifact's signature says. Each non-empty supported field constrains independently; an empty or absent catalog provenance preserves ordinary trust on first use. For multi-signed OCI artifacts, every cryptographically valid signature is considered until one satisfies all declared constraints. A lock entry, including a legacy entry with no recorded trust state, always takes precedence over the catalog — the catalog only fills the gap before any trust decision has been recorded. User-scoped installs do not use project lock-file verification and therefore do not apply catalog provenance. + +Catalog constraints the skill verifier cannot enforce are refused rather than silently skipped. Attestations are not yet compared, and `sigstore_url` selects trust material in the registry schema while the skill verifier currently uses its embedded public-good trust root. A non-empty `sigstore_url` or any declared `attestation` therefore fails a true first project install; existing lock entries remain authoritative. The Sigstore policy alone is not the whole guarantee: its SAN match deliberately leaves the signing workflow's git ref unpinned (`(@.*)?$`), so identity alone is satisfied by "the right workflow, on any branch." Two additional certificate fields — the git ref the signing workflow ran on and the runner class it executed in (`repositoryRef:`/`runnerEnvironment:` in `provenance:`) — are enforced separately, after the Sigstore policy succeeds, against the certificate's Fulcio extensions. An entry recorded before these fields existed, or a certificate that carries neither (a signer outside CI), is unconstrained on that field — never a wildcard match once something IS recorded. Install, sync, and upgrade all require the recorded ref and runner class to match exactly, with no automatic allowance for any kind of change, ref rotation included: an earlier version of this guard let a recorded tag ref rotate to any other tag ref automatically, reasoning that a release workflow signs each version on its own tag, but review found that this let a candidate signed from an attacker's own tag on the same repository (e.g. `refs/tags/attacker-release`) replace a pinned tag just as easily, since nothing tied the candidate's tag to the version actually being upgraded to. A ref or runner-class change of any shape is now blocked exactly like a genuine signer-identity change, and needs the same explicit `--allow-signer-change` to proceed and re-record it. diff --git a/pkg/skills/options.go b/pkg/skills/options.go index c63a0e84b4..cac6099f79 100644 --- a/pkg/skills/options.go +++ b/pkg/skills/options.go @@ -3,6 +3,8 @@ package skills +import regtypes "github.com/stacklok/toolhive-core/registry/types" + // ListOptions configures the behavior of the List operation. type ListOptions struct { // Scope filters results by installation scope. @@ -86,14 +88,13 @@ type InstallOptions struct { // install-time verification; recorded as `unsigned: true` in the lock // entry. Unsigned bool `json:"-"` - // CatalogProvenance is the expected signer identity declared by the - // skill registry/catalog entry this install resolved from, if any. When - // no lock entry already exists for this skill, install-time - // verification checks the artifact against this instead of blindly - // trusting whatever it observes (trust-on-first-use) — see RFC - // THV-0080 follow-up #6310. A lock entry, once one exists, always takes - // precedence over this. Internal use only — NOT exposed via HTTP API. - CatalogProvenance *ProvenanceInfo `json:"-"` + // CatalogProvenance is the independently optional provenance constraints + // declared by the skill registry/catalog entry this install resolved + // from. On true first use, install-time verification checks each non-empty + // field against the observed signature. A lock entry, including a legacy + // entry with no trust state, always takes precedence over this. Internal + // use only — NOT exposed via HTTP API. + CatalogProvenance *regtypes.Provenance `json:"-"` // Provenance carries the verified signer identity established during // install-time verification, for recording into the lock entry. Set by // the verification step, nil when the artifact is unsigned or diff --git a/pkg/skills/skillsvc/install.go b/pkg/skills/skillsvc/install.go index baf4ed251e..bf911e7dc7 100644 --- a/pkg/skills/skillsvc/install.go +++ b/pkg/skills/skillsvc/install.go @@ -207,11 +207,11 @@ func (s *service) installFromResolvedRegistry( deps *depState, alreadyLocked bool, ) (*skills.InstallResult, error) { - catalogProvenance, provErr := provenanceInfoFromCatalog(resolved.Provenance) - if provErr != nil { - return nil, provErr - } - opts.CatalogProvenance = catalogProvenance + // Carry catalog constraints to the verification boundary unchanged. + // Validation is deliberately deferred until the resolved canonical skill + // name can be checked against the lock: existing lock entries take + // precedence, and user-scope installs do not apply project trust policy. + opts.CatalogProvenance = resolved.Provenance switch { case resolved.OCIRef != nil: diff --git a/pkg/skills/skillsvc/list.go b/pkg/skills/skillsvc/list.go index a52ee22567..ee05b3f922 100644 --- a/pkg/skills/skillsvc/list.go +++ b/pkg/skills/skillsvc/list.go @@ -84,7 +84,7 @@ func (s *service) Info(ctx context.Context, opts skills.InfoOptions) (*skills.Sk // Project-scoped, lock-managed skills carry the lock file's recorded // trust state so callers can display what installs are checked against. if scope == skills.ScopeProject && projectRoot != "" { - if expected, expectUnsigned, trustErr := expectedLockTrust(projectRoot, opts.Name); trustErr == nil { + if expected, expectUnsigned, _, trustErr := expectedLockTrust(projectRoot, opts.Name); trustErr == nil { info.Provenance = provenanceInfoFromLock(expected) info.Unsigned = expectUnsigned } diff --git a/pkg/skills/skillsvc/upgrade_verify_test.go b/pkg/skills/skillsvc/upgrade_verify_test.go index 4a7426f466..d6e2f491d4 100644 --- a/pkg/skills/skillsvc/upgrade_verify_test.go +++ b/pkg/skills/skillsvc/upgrade_verify_test.go @@ -40,7 +40,7 @@ func signerChangeFixture( mv := verifiermocks.NewMockVerifier(gomock.NewController(t)) mv.EXPECT().VerifyGit(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). AnyTimes(). - DoAndReturn(func(_ any, _, _ []byte, expected *lockfile.Provenance) (*verifier.Result, error) { + DoAndReturn(func(_ any, _, _ []byte, expected *verifier.ProvenanceExpectation) (*verifier.Result, error) { installs++ if installs == 1 { return signedResult(), nil // initial install (TOFU) @@ -49,7 +49,7 @@ func signerChangeFixture( if err != nil { return nil, err } - if expected != nil && expected.SignerIdentity != result.SignerIdentity { + if expected != nil && result.SignerIdentity != testSignerIdentity { return nil, verifier.ErrSignerMismatch } return result, nil @@ -100,7 +100,7 @@ func TestUpgrade_RefChangeRequiresAllowSignerChange(t *testing.T) { mv := verifiermocks.NewMockVerifier(gomock.NewController(t)) mv.EXPECT().VerifyGit(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). AnyTimes(). - DoAndReturn(func(_ any, _, _ []byte, expected *lockfile.Provenance) (*verifier.Result, error) { + DoAndReturn(func(_ any, _, _ []byte, expected *verifier.ProvenanceExpectation) (*verifier.Result, error) { calls++ if calls == 1 { return refSignedResult(installedRef), nil // initial install (TOFU) @@ -109,10 +109,7 @@ func TestUpgrade_RefChangeRequiresAllowSignerChange(t *testing.T) { if expected == nil { return candidate, nil // the upgrade's plan-time signer probe } - if expected.RepositoryRef != "" && expected.RepositoryRef != candidate.RepositoryRef { - return nil, verifier.ErrSignerMismatch - } - return candidate, nil + return nil, verifier.ErrSignerMismatch }) mv.EXPECT().VerifyBundleOffline(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().Return(nil) @@ -223,7 +220,7 @@ func TestUpgrade_RefTransitionBlocked(t *testing.T) { mv := verifiermocks.NewMockVerifier(gomock.NewController(t)) mv.EXPECT().VerifyGit(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). AnyTimes(). - DoAndReturn(func(_ any, _, _ []byte, expected *lockfile.Provenance) (*verifier.Result, error) { + DoAndReturn(func(_ any, _, _ []byte, expected *verifier.ProvenanceExpectation) (*verifier.Result, error) { calls++ if calls == 1 { return refSignedResult(tc.lockedRef), nil // initial install (TOFU) diff --git a/pkg/skills/skillsvc/verify.go b/pkg/skills/skillsvc/verify.go index 067103a950..11c528fb85 100644 --- a/pkg/skills/skillsvc/verify.go +++ b/pkg/skills/skillsvc/verify.go @@ -66,24 +66,35 @@ func (s *service) verifyOCIInstall( opts skills.InstallOptions, skillName, ref, digest string, ) (*provenanceDecision, error) { - expected, expectUnsigned, err := expectedLockTrust(opts.ProjectRoot, skillName) + expected, expectUnsigned, lockEntryExists, err := expectedLockTrust(opts.ProjectRoot, skillName) if err != nil { return nil, err } - if expected == nil && !expectUnsigned { - expected = provenanceInfoToLock(opts.CatalogProvenance) + var catalogExpected *regtypes.Provenance + verifierExpected := verifier.NewLockExpectation(expected) + if !lockEntryExists { + if err := validateCatalogProvenance(opts.CatalogProvenance); err != nil { + return nil, err + } + catalogExpected = normalizeCatalogProvenance(opts.CatalogProvenance) + verifierExpected = verifier.NewCatalogExpectation(catalogExpected) } if opts.AllowSignerChange { // The signer-change guard was explicitly overridden: verify the // chain of trust only and re-record whatever identity is observed. expected, expectUnsigned = nil, false + catalogExpected = nil + verifierExpected = nil } if expectUnsigned { return unsignedLockedDecision(opts, skillName) } - result, verifyErr := s.artifactVerifier().VerifyOCI(ctx, ref, digest, expected) + result, verifyErr := s.artifactVerifier().VerifyOCI(ctx, ref, digest, verifierExpected) if verifyErr != nil { + if catalogExpected != nil { + return nil, classifyCatalogVerifyError(verifyErr, skillName) + } if isAllowedUnsigned(verifyErr, opts, expected) { return &provenanceDecision{unsigned: true}, nil } @@ -105,22 +116,33 @@ func (s *service) verifyGitInstall( payload []byte, signature string, ) (*provenanceDecision, error) { - expected, expectUnsigned, err := expectedLockTrust(opts.ProjectRoot, skillName) + expected, expectUnsigned, lockEntryExists, err := expectedLockTrust(opts.ProjectRoot, skillName) if err != nil { return nil, err } - if expected == nil && !expectUnsigned { - expected = provenanceInfoToLock(opts.CatalogProvenance) + var catalogExpected *regtypes.Provenance + verifierExpected := verifier.NewLockExpectation(expected) + if !lockEntryExists { + if err := validateCatalogProvenance(opts.CatalogProvenance); err != nil { + return nil, err + } + catalogExpected = normalizeCatalogProvenance(opts.CatalogProvenance) + verifierExpected = verifier.NewCatalogExpectation(catalogExpected) } if opts.AllowSignerChange { expected, expectUnsigned = nil, false + catalogExpected = nil + verifierExpected = nil } if expectUnsigned { return unsignedLockedDecision(opts, skillName) } - result, verifyErr := s.artifactVerifier().VerifyGit(ctx, payload, []byte(signature), expected) + result, verifyErr := s.artifactVerifier().VerifyGit(ctx, payload, []byte(signature), verifierExpected) if verifyErr != nil { + if catalogExpected != nil { + return nil, classifyCatalogVerifyError(verifyErr, skillName) + } if isAllowedUnsigned(verifyErr, opts, expected) { return &provenanceDecision{unsigned: true}, nil } @@ -139,7 +161,7 @@ func (s *service) verifyGitInstall( // artifact for a local build is exactly the substitution the lock exists to // catch. func verifyLocalInstall(opts skills.InstallOptions, skillName string) (*provenanceDecision, error) { - expected, expectUnsigned, err := expectedLockTrust(opts.ProjectRoot, skillName) + expected, expectUnsigned, _, err := expectedLockTrust(opts.ProjectRoot, skillName) if err != nil { return nil, err } @@ -184,28 +206,29 @@ func unsignedLockedDecision(opts skills.InstallOptions, skillName string) (*prov } // expectedLockTrust reads the trust state recorded in projectRoot's lock -// file for skillName: the expected signer identity (nil on first use — the -// TOFU case), or that the entry was recorded unsigned. -func expectedLockTrust(projectRoot, skillName string) (*lockfile.Provenance, bool, error) { +// file for skillName: the expected signer identity, whether the entry was +// recorded unsigned, and whether any entry exists. The third result +// distinguishes true first use from legacy entries that predate trust state. +func expectedLockTrust(projectRoot, skillName string) (*lockfile.Provenance, bool, bool, error) { if projectRoot == "" { - return nil, false, nil + return nil, false, false, nil } root, err := lockfile.OpenRoot(projectRoot) if err != nil { - return nil, false, err + return nil, false, false, err } lf, err := lockfile.Load(root) if err != nil { - return nil, false, err + return nil, false, false, err } entry, ok := lf.Get(skillName) if !ok { - return nil, false, nil + return nil, false, false, nil } if entry.Unsigned { - return nil, true, nil + return nil, true, true, nil } - return entry.Provenance, false, nil + return entry.Provenance, false, true, nil } // isAllowedUnsigned reports whether a verification failure is the unsigned @@ -277,6 +300,21 @@ func classifyInstallVerifyError( } } +// classifyCatalogVerifyError reports a first-install artifact that does not +// satisfy the provenance constraints declared by its catalog entry. +func classifyCatalogVerifyError(verifyErr error, skillName string) error { + if errors.Is(verifyErr, verifier.ErrUnsigned) { + return httperr.WithCode( + fmt.Errorf("skill %q is unsigned but its catalog entry requires verified provenance", skillName), + http.StatusForbidden, + ) + } + return httperr.WithCode( + fmt.Errorf("skill %q does not match its catalog-declared provenance: %w", skillName, verifyErr), + http.StatusForbidden, + ) +} + // classifySignatureError maps verifier sentinels to typed failure reasons // for sync/upgrade results. Returns "" when err is not a signature failure. func classifySignatureError(err error) skills.FailureReason { @@ -346,35 +384,41 @@ func provenanceInfoFromResult(r *verifier.Result) *skills.ProvenanceInfo { } } -// provenanceInfoFromCatalog converts a skill registry/catalog entry's -// declared provenance into the internal shape used as the expected identity -// for first-install verification (RFC THV-0080 follow-up #6310). nil input -// yields nil output — most catalog entries won't declare one for a while, -// and that must not block installs. -// -// An attestation constraint is refused rather than silently dropped: -// toolhive's skill verifier (pkg/skills/verifier) has no attestation -// comparison logic today, so honoring the rest of the provenance while -// quietly ignoring Attestation would let a catalog author believe a -// guarantee is enforced when it is not — the same fail-closed principle -// toolhive-core's own Skill.Validate applies to catalog authors. -func provenanceInfoFromCatalog(p *regtypes.Provenance) (*skills.ProvenanceInfo, error) { +// validateCatalogProvenance rejects catalog constraints the skills verifier +// cannot currently enforce. It runs only for project-scope true first use, +// after lock precedence has been established. +func validateCatalogProvenance(p *regtypes.Provenance) error { if p == nil { - return nil, nil + return nil + } + if p.SigstoreURL != "" { + return httperr.WithCode( + errors.New("catalog declares a sigstore_url constraint for this skill,"+ + " which toolhive cannot yet enforce; refusing to silently ignore it"), + http.StatusUnprocessableEntity, + ) } if p.Attestation != nil { - return nil, httperr.WithCode( + return httperr.WithCode( errors.New("catalog declares an attestation constraint for this skill,"+ " which toolhive cannot yet enforce; refusing to silently ignore it"), http.StatusUnprocessableEntity, ) } - return &skills.ProvenanceInfo{ - SignerIdentity: p.SignerIdentity, - CertIssuer: p.CertIssuer, - RepositoryURI: p.RepositoryURI, - RepositoryRef: p.RepositoryRef, - RunnerEnvironment: p.RunnerEnvironment, - SigstoreURL: p.SigstoreURL, - }, nil + return nil +} + +// normalizeCatalogProvenance treats an empty supported constraint as absent. +// Catalog fields constrain independently, so an object containing no +// supported values must preserve the ordinary unconstrained TOFU behavior, +// including the explicit allow-unsigned path. +func normalizeCatalogProvenance(p *regtypes.Provenance) *regtypes.Provenance { + if p == nil || (p.SignerIdentity == "" && + p.CertIssuer == "" && + p.RepositoryURI == "" && + p.RepositoryRef == "" && + p.RunnerEnvironment == "") { + return nil + } + return p } diff --git a/pkg/skills/skillsvc/verify_test.go b/pkg/skills/skillsvc/verify_test.go index dea185e79d..0a3cc1ee0c 100644 --- a/pkg/skills/skillsvc/verify_test.go +++ b/pkg/skills/skillsvc/verify_test.go @@ -61,6 +61,15 @@ func loadLockEntry(t *testing.T, projectRoot, name string) (lockfile.Entry, bool return lf.Get(name) } +// writeLockEntry seeds project trust state without first passing through the +// install path under test. +func writeLockEntry(t *testing.T, projectRoot string, entry lockfile.Entry) { + t.Helper() + root, err := lockfile.OpenRoot(projectRoot) + require.NoError(t, err) + require.NoError(t, lockfile.UpsertEntry(root, entry)) +} + //nolint:paralleltest // uses t.Setenv via newLockTestService, incompatible with t.Parallel func TestInstallVerification_TOFURecordsProvenance(t *testing.T) { gr, fixtures := newGitResolverMock(t) @@ -93,9 +102,9 @@ func TestInstallVerification_TOFURecordsProvenance(t *testing.T) { // Second install: the recorded identity must flow into the verifier as // the expected identity. mv.EXPECT().VerifyGit(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). - DoAndReturn(func(_ any, _, _ []byte, expected *lockfile.Provenance) (*verifier.Result, error) { + DoAndReturn(func(_ any, _, _ []byte, expected *verifier.ProvenanceExpectation) (*verifier.Result, error) { require.NotNil(t, expected, "the second install must enforce the recorded identity") - assert.Equal(t, testSignerIdentity, expected.SignerIdentity) + assert.Equal(t, verifier.NewLockExpectation(entry.Provenance), expected) return signedResult(), nil }) _, err = svc.Install(t.Context(), skills.InstallOptions{ @@ -148,13 +157,13 @@ func TestInstallVerification_CatalogProvenanceUsedOnFirstUse(t *testing.T) { }, nil) mv := verifiermocks.NewMockVerifier(gomock.NewController(t)) - // No lock entry exists yet, but the registry entry declares an expected - // identity: it must reach the verifier instead of nil (blind TOFU). + // No lock entry exists yet, so the catalog expectation and its wildcard + // semantics must reach the verifier distinctly from strict lock trust. + catalogExpected := ®types.Provenance{SignerIdentity: testSignerIdentity, CertIssuer: testCertIssuer} mv.EXPECT().VerifyGit(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). - DoAndReturn(func(_ any, _, _ []byte, expected *lockfile.Provenance) (*verifier.Result, error) { + DoAndReturn(func(_ any, _, _ []byte, expected *verifier.ProvenanceExpectation) (*verifier.Result, error) { require.NotNil(t, expected, "a catalog-declared provenance must be checked on first install") - assert.Equal(t, testSignerIdentity, expected.SignerIdentity) - assert.Equal(t, testCertIssuer, expected.CertIssuer) + assert.Equal(t, verifier.NewCatalogExpectation(catalogExpected), expected) return signedResult(), nil }) @@ -175,56 +184,420 @@ func TestInstallVerification_CatalogProvenanceUsedOnFirstUse(t *testing.T) { //nolint:paralleltest // uses t.Setenv via newLockTestService, incompatible with t.Parallel func TestInstallVerification_LockEntryTakesPrecedenceOverCatalog(t *testing.T) { gr := gitmocks.NewMockResolver(gomock.NewController(t)) - gr.EXPECT().Resolve(gomock.Any(), gomock.Any()).Return(catalogSkillFiles(), nil).AnyTimes() + gr.EXPECT().Resolve(gomock.Any(), gomock.Any()).Return(catalogSkillFiles(), nil) lookup := regmocks.NewMockProvider(gomock.NewController(t)) - // The catalog now declares a DIFFERENT identity than what will end up - // locked — if the lock entry didn't win, this install would enforce the - // wrong expectation. + // The catalog declares a different identity from the pre-existing lock. lookup.EXPECT().SearchSkills("catalog-skill").Return([]regtypes.Skill{ catalogSkill(®types.Provenance{SignerIdentity: "/.github/workflows/other.yml", CertIssuer: testCertIssuer}), - }, nil).AnyTimes() + }, nil) mv := verifiermocks.NewMockVerifier(gomock.NewController(t)) - // First install: no lock entry yet, so the catalog identity is checked - // and — since it verifies — becomes what gets locked. - mv.EXPECT().VerifyGit(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). - Return(&verifier.Result{ - Signed: true, SignerIdentity: "/.github/workflows/other.yml", CertIssuer: testCertIssuer, - SigstoreURL: "https://rekor.sigstore.dev", Bundle: []byte(`{"bundle":true}`), - }, nil) - svc, projectRoot := newLockTestService(t, gr, WithVerifier(mv), WithSkillLookup(lookup)) - _, err := svc.Install(t.Context(), skills.InstallOptions{ - Name: "catalog-skill", - Scope: skills.ScopeProject, - ProjectRoot: projectRoot, - Clients: []string{"claude-code"}, + writeLockEntry(t, projectRoot, lockfile.Entry{ + Name: "catalog-skill", + Source: "catalog-skill", + ResolvedReference: "git://github.com/test/catalog-skill", + Digest: testCommitHash, + Explicit: true, + Provenance: &lockfile.Provenance{ + SignerIdentity: testSignerIdentity, + CertIssuer: testCertIssuer, + }, }) - require.NoError(t, err) - // Reinstall: the lock entry now recorded is what must be enforced, not - // the catalog's declared identity. mv.EXPECT().VerifyGit(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). - DoAndReturn(func(_ any, _, _ []byte, expected *lockfile.Provenance) (*verifier.Result, error) { + DoAndReturn(func(_ any, _, _ []byte, expected *verifier.ProvenanceExpectation) (*verifier.Result, error) { require.NotNil(t, expected) - assert.Equal(t, "/.github/workflows/other.yml", expected.SignerIdentity, + assert.Equal(t, verifier.NewLockExpectation(&lockfile.Provenance{ + SignerIdentity: testSignerIdentity, + CertIssuer: testCertIssuer, + }), expected, "the lock entry, not the catalog, must be enforced once one exists") - return &verifier.Result{ - Signed: true, SignerIdentity: "/.github/workflows/other.yml", CertIssuer: testCertIssuer, - SigstoreURL: "https://rekor.sigstore.dev", Bundle: []byte(`{"bundle":true}`), - }, nil + return signedResult(), nil }) - _, err = svc.Install(t.Context(), skills.InstallOptions{ + _, err := svc.Install(t.Context(), skills.InstallOptions{ Name: "catalog-skill", Scope: skills.ScopeProject, ProjectRoot: projectRoot, Clients: []string{"claude-code"}, - Force: true, }) require.NoError(t, err) } +func TestVerifyInstall_CatalogPartialConstraints(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + provenance *regtypes.Provenance + observed *verifier.Result + }{ + {name: "empty provenance constrains nothing", provenance: ®types.Provenance{}, observed: signedResult()}, + {name: "signer only", provenance: ®types.Provenance{SignerIdentity: testSignerIdentity}, observed: signedResult()}, + {name: "issuer only", provenance: ®types.Provenance{CertIssuer: testCertIssuer}, observed: signedResult()}, + {name: "repository only", provenance: ®types.Provenance{RepositoryURI: "https://github.com/org/repo"}, observed: signedResult()}, + {name: "ref only", provenance: ®types.Provenance{RepositoryRef: "refs/tags/v1.0.0"}, observed: refSignedResult("refs/tags/v1.0.0")}, + {name: "runner only", provenance: ®types.Provenance{RunnerEnvironment: testRunnerEnvironment}, observed: refSignedResult("refs/tags/v1.0.0")}, + } + for _, backend := range []string{"git", "oci"} { + for _, tc := range tests { + t.Run(backend+"/"+tc.name, func(t *testing.T) { + t.Parallel() + projectRoot := makeProjectRoot(t) + mv := verifiermocks.NewMockVerifier(gomock.NewController(t)) + svc := &service{sigVerifier: mv} + opts := skills.InstallOptions{ + ProjectRoot: projectRoot, + CatalogProvenance: tc.provenance, + } + + var ( + decision *provenanceDecision + err error + ) + wantExpected := verifier.NewCatalogExpectation(tc.provenance) + if tc.provenance.SignerIdentity == "" && tc.provenance.CertIssuer == "" && + tc.provenance.RepositoryURI == "" && tc.provenance.RepositoryRef == "" && + tc.provenance.RunnerEnvironment == "" { + wantExpected = nil + } + if backend == "git" { + mv.EXPECT().VerifyGit(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Eq(wantExpected)). + Return(tc.observed, nil) + decision, err = svc.verifyGitInstall(t.Context(), opts, "catalog-skill", []byte("payload"), "signature") + } else { + mv.EXPECT().VerifyOCI( + gomock.Any(), "ghcr.io/test/catalog-skill:v1", "sha256:digest", gomock.Eq(wantExpected)). + Return(tc.observed, nil) + decision, err = svc.verifyOCIInstall( + t.Context(), opts, "catalog-skill", "ghcr.io/test/catalog-skill:v1", "sha256:digest") + } + require.NoError(t, err) + require.NotNil(t, decision.provenance) + assert.Equal(t, tc.observed.SignerIdentity, decision.provenance.SignerIdentity) + }) + } + } +} + +func TestVerifyInstall_CatalogPartialConstraintMismatchRejected(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + provenance *regtypes.Provenance + }{ + {name: "signer", provenance: ®types.Provenance{SignerIdentity: "attacker@example.com"}}, + {name: "issuer", provenance: ®types.Provenance{CertIssuer: "https://issuer.example.com"}}, + {name: "repository", provenance: ®types.Provenance{RepositoryURI: "https://github.com/attacker/repo"}}, + {name: "ref", provenance: ®types.Provenance{RepositoryRef: "refs/heads/attacker"}}, + {name: "runner", provenance: ®types.Provenance{RunnerEnvironment: "self-hosted"}}, + } + for _, backend := range []string{"git", "oci"} { + for _, tc := range tests { + t.Run(backend+"/"+tc.name, func(t *testing.T) { + t.Parallel() + projectRoot := makeProjectRoot(t) + mv := verifiermocks.NewMockVerifier(gomock.NewController(t)) + svc := &service{sigVerifier: mv} + opts := skills.InstallOptions{ProjectRoot: projectRoot, CatalogProvenance: tc.provenance} + wantExpected := verifier.NewCatalogExpectation(tc.provenance) + + var err error + if backend == "git" { + mv.EXPECT().VerifyGit(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Eq(wantExpected)). + Return(nil, verifier.ErrSignerMismatch) + _, err = svc.verifyGitInstall(t.Context(), opts, "catalog-skill", []byte("payload"), "signature") + } else { + mv.EXPECT().VerifyOCI(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Eq(wantExpected)). + Return(nil, verifier.ErrSignerMismatch) + _, err = svc.verifyOCIInstall( + t.Context(), opts, "catalog-skill", "ghcr.io/test/catalog-skill:v1", "sha256:digest") + } + require.Error(t, err) + assert.Equal(t, http.StatusForbidden, httperr.Code(err)) + }) + } + } +} + +func TestVerifyInstall_CatalogIdentityPairPreservesCatalogSemantics(t *testing.T) { + t.Parallel() + + for _, backend := range []string{"git", "oci"} { + t.Run(backend, func(t *testing.T) { + t.Parallel() + projectRoot := makeProjectRoot(t) + mv := verifiermocks.NewMockVerifier(gomock.NewController(t)) + svc := &service{sigVerifier: mv} + opts := skills.InstallOptions{ + ProjectRoot: projectRoot, + CatalogProvenance: ®types.Provenance{ + SignerIdentity: testSignerIdentity, + CertIssuer: testCertIssuer, + }, + } + checkExpected := func(expected *verifier.ProvenanceExpectation) (*verifier.Result, error) { + require.NotNil(t, expected) + assert.Equal(t, verifier.NewCatalogExpectation(opts.CatalogProvenance), expected) + return signedResult(), nil + } + + var err error + if backend == "git" { + mv.EXPECT().VerifyGit(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(_ any, _, _ []byte, expected *verifier.ProvenanceExpectation) (*verifier.Result, error) { + return checkExpected(expected) + }) + _, err = svc.verifyGitInstall(t.Context(), opts, "catalog-skill", []byte("payload"), "signature") + } else { + mv.EXPECT().VerifyOCI(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(_ any, _, _ string, expected *verifier.ProvenanceExpectation) (*verifier.Result, error) { + return checkExpected(expected) + }) + _, err = svc.verifyOCIInstall( + t.Context(), opts, "catalog-skill", "ghcr.io/test/catalog-skill:v1", "sha256:digest") + } + require.NoError(t, err) + }) + } +} + +func TestVerifyInstall_CatalogConstraintCannotAllowUnsigned(t *testing.T) { + t.Parallel() + + for _, backend := range []string{"git", "oci"} { + t.Run(backend, func(t *testing.T) { + t.Parallel() + projectRoot := makeProjectRoot(t) + mv := verifiermocks.NewMockVerifier(gomock.NewController(t)) + svc := &service{sigVerifier: mv} + opts := skills.InstallOptions{ + ProjectRoot: projectRoot, + AllowUnsigned: true, + CatalogProvenance: ®types.Provenance{SignerIdentity: testSignerIdentity}, + } + + var err error + if backend == "git" { + mv.EXPECT().VerifyGit( + gomock.Any(), gomock.Any(), gomock.Any(), gomock.Eq(verifier.NewCatalogExpectation(opts.CatalogProvenance))). + Return(nil, verifier.ErrUnsigned) + _, err = svc.verifyGitInstall(t.Context(), opts, "catalog-skill", []byte("payload"), "") + } else { + mv.EXPECT().VerifyOCI( + gomock.Any(), gomock.Any(), gomock.Any(), gomock.Eq(verifier.NewCatalogExpectation(opts.CatalogProvenance))). + Return(nil, verifier.ErrUnsigned) + _, err = svc.verifyOCIInstall( + t.Context(), opts, "catalog-skill", "ghcr.io/test/catalog-skill:v1", "sha256:digest") + } + require.Error(t, err) + assert.Equal(t, http.StatusForbidden, httperr.Code(err)) + }) + } +} + +func TestVerifyInstall_EmptyCatalogProvenanceAllowsUnsigned(t *testing.T) { + t.Parallel() + + for _, backend := range []string{"git", "oci"} { + t.Run(backend, func(t *testing.T) { + t.Parallel() + projectRoot := makeProjectRoot(t) + mv := verifiermocks.NewMockVerifier(gomock.NewController(t)) + svc := &service{sigVerifier: mv} + opts := skills.InstallOptions{ + ProjectRoot: projectRoot, + AllowUnsigned: true, + CatalogProvenance: ®types.Provenance{}, + } + + var ( + decision *provenanceDecision + err error + ) + if backend == "git" { + mv.EXPECT().VerifyGit(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Nil()). + Return(nil, verifier.ErrUnsigned) + decision, err = svc.verifyGitInstall(t.Context(), opts, "catalog-skill", []byte("payload"), "") + } else { + mv.EXPECT().VerifyOCI(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Nil()). + Return(nil, verifier.ErrUnsigned) + decision, err = svc.verifyOCIInstall( + t.Context(), opts, "catalog-skill", "ghcr.io/test/catalog-skill:v1", "sha256:digest") + } + require.NoError(t, err) + assert.True(t, decision.unsigned) + }) + } +} + +func TestVerifyInstall_UnsupportedCatalogConstraintsOnlyFailOnFirstUse(t *testing.T) { + t.Parallel() + + constraints := []struct { + name string + provenance *regtypes.Provenance + }{ + { + name: "attestation", + provenance: ®types.Provenance{ + Attestation: ®types.VerifiedAttestation{PredicateType: "https://slsa.dev/provenance/v1"}, + }, + }, + { + name: "sigstore URL", + provenance: ®types.Provenance{SigstoreURL: "https://sigstore.example.com/root.json"}, + }, + } + states := []struct { + name string + entry *lockfile.Entry + wantErr bool + wantExpected bool + }{ + {name: "first use", wantErr: true}, + { + name: "signed lock", + entry: &lockfile.Entry{ + Name: "catalog-skill", + Source: "catalog-skill", + Digest: testCommitHash, + Provenance: &lockfile.Provenance{ + SignerIdentity: testSignerIdentity, + CertIssuer: testCertIssuer, + }, + }, + wantExpected: true, + }, + { + name: "legacy lock without trust state", + entry: &lockfile.Entry{ + Name: "catalog-skill", + Source: "catalog-skill", + Digest: testCommitHash, + }, + }, + } + for _, backend := range []string{"git", "oci"} { + for _, constraint := range constraints { + for _, state := range states { + t.Run(backend+"/"+constraint.name+"/"+state.name, func(t *testing.T) { + t.Parallel() + projectRoot := makeProjectRoot(t) + if state.entry != nil { + writeLockEntry(t, projectRoot, *state.entry) + } + mv := verifiermocks.NewMockVerifier(gomock.NewController(t)) + svc := &service{sigVerifier: mv} + opts := skills.InstallOptions{ + ProjectRoot: projectRoot, + CatalogProvenance: constraint.provenance, + } + + if !state.wantErr { + checkExpected := func(expected *verifier.ProvenanceExpectation) (*verifier.Result, error) { + if state.wantExpected { + require.NotNil(t, expected) + assert.Equal(t, verifier.NewLockExpectation(state.entry.Provenance), expected) + } else { + assert.Nil(t, expected, + "a legacy lock must suppress catalog policy without inventing lock trust") + } + return signedResult(), nil + } + if backend == "git" { + mv.EXPECT().VerifyGit(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(_ any, _, _ []byte, expected *verifier.ProvenanceExpectation) (*verifier.Result, error) { + return checkExpected(expected) + }) + } else { + mv.EXPECT().VerifyOCI(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(_ any, _, _ string, expected *verifier.ProvenanceExpectation) (*verifier.Result, error) { + return checkExpected(expected) + }) + } + } + + var err error + if backend == "git" { + _, err = svc.verifyGitInstall(t.Context(), opts, "catalog-skill", []byte("payload"), "signature") + } else { + _, err = svc.verifyOCIInstall( + t.Context(), opts, "catalog-skill", "ghcr.io/test/catalog-skill:v1", "sha256:digest") + } + if state.wantErr { + require.Error(t, err) + assert.Equal(t, http.StatusUnprocessableEntity, httperr.Code(err)) + return + } + require.NoError(t, err) + }) + } + } + } +} + +//nolint:paralleltest // uses t.Setenv via newLockTestService, incompatible with t.Parallel +func TestInstallVerification_CatalogRejectionHasNoSideEffects(t *testing.T) { + tests := []struct { + name string + provenance *regtypes.Provenance + verifyErr error + wantCode int + }{ + { + name: "provenance mismatch", + provenance: ®types.Provenance{SignerIdentity: "attacker@example.com"}, + verifyErr: verifier.ErrSignerMismatch, + wantCode: http.StatusForbidden, + }, + { + name: "unsupported sigstore URL", + provenance: ®types.Provenance{SigstoreURL: "https://sigstore.example.com/root.json"}, + wantCode: http.StatusUnprocessableEntity, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + gr := gitmocks.NewMockResolver(gomock.NewController(t)) + gr.EXPECT().Resolve(gomock.Any(), gomock.Any()).Return(catalogSkillFiles(), nil) + + lookup := regmocks.NewMockProvider(gomock.NewController(t)) + lookup.EXPECT().SearchSkills("catalog-skill").Return([]regtypes.Skill{ + catalogSkill(tc.provenance), + }, nil) + + mv := verifiermocks.NewMockVerifier(gomock.NewController(t)) + if tc.verifyErr != nil { + mv.EXPECT().VerifyGit( + gomock.Any(), gomock.Any(), gomock.Any(), gomock.Eq(verifier.NewCatalogExpectation(tc.provenance))). + Return(nil, tc.verifyErr) + } + svc, projectRoot := newLockTestService(t, gr, WithVerifier(mv), WithSkillLookup(lookup)) + + _, err := svc.Install(t.Context(), skills.InstallOptions{ + Name: "catalog-skill", + Scope: skills.ScopeProject, + ProjectRoot: projectRoot, + Clients: []string{"claude-code"}, + }) + require.Error(t, err) + assert.Equal(t, tc.wantCode, httperr.Code(err)) + + _, ok := loadLockEntry(t, projectRoot, "catalog-skill") + assert.False(t, ok, "a rejected catalog policy must not write a lock entry") + _, err = svc.Info(t.Context(), skills.InfoOptions{ + Name: "catalog-skill", Scope: skills.ScopeProject, ProjectRoot: projectRoot, + }) + require.Error(t, err, "a rejected catalog policy must not create a database record") + assert.NoDirExists(t, projectRoot+"/.claude/skills/catalog-skill", + "verification must fail before skill files are extracted") + }) + } +} + //nolint:paralleltest // uses t.Setenv via newLockTestService, incompatible with t.Parallel func TestInstallVerification_UnsignedRejectedWithoutFlag(t *testing.T) { gr, fixtures := newGitResolverMock(t) @@ -345,9 +718,9 @@ func TestInstallVerification_EnforcesPinnedRef(t *testing.T) { require.NoError(t, err) mv.EXPECT().VerifyGit(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). - DoAndReturn(func(_ any, _, _ []byte, expected *lockfile.Provenance) (*verifier.Result, error) { + DoAndReturn(func(_ any, _, _ []byte, expected *verifier.ProvenanceExpectation) (*verifier.Result, error) { require.NotNil(t, expected) - assert.Equal(t, "refs/tags/v0.1.0", expected.RepositoryRef, + assert.Equal(t, verifier.NewLockExpectation(entry.Provenance), expected, "install must enforce the recorded ref, not relax it like upgrade") return nil, verifier.ErrSignerMismatch }) @@ -413,6 +786,47 @@ func TestInstallVerification_UserScopeSkipsVerification(t *testing.T) { require.NoError(t, err) } +//nolint:paralleltest // uses t.Setenv via newLockTestService, incompatible with t.Parallel +func TestInstallVerification_UserScopeIgnoresUnsupportedCatalogConstraints(t *testing.T) { + tests := []struct { + name string + provenance *regtypes.Provenance + }{ + { + name: "attestation", + provenance: ®types.Provenance{ + Attestation: ®types.VerifiedAttestation{PredicateType: "https://slsa.dev/provenance/v1"}, + }, + }, + { + name: "sigstore URL", + provenance: ®types.Provenance{SigstoreURL: "https://sigstore.example.com/root.json"}, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + gr := gitmocks.NewMockResolver(gomock.NewController(t)) + gr.EXPECT().Resolve(gomock.Any(), gomock.Any()).Return(catalogSkillFiles(), nil) + + lookup := regmocks.NewMockProvider(gomock.NewController(t)) + lookup.EXPECT().SearchSkills("catalog-skill").Return([]regtypes.Skill{ + catalogSkill(tc.provenance), + }, nil) + + // Unsupported project verification policy is irrelevant at user + // scope, where no verifier or lock trust decision is used. + mv := verifiermocks.NewMockVerifier(gomock.NewController(t)) + svc, _ := newLockTestService(t, gr, WithVerifier(mv), WithSkillLookup(lookup)) + _, err := svc.Install(t.Context(), skills.InstallOptions{ + Name: "catalog-skill", + Scope: skills.ScopeUser, + Clients: []string{"claude-code"}, + }) + require.NoError(t, err) + }) + } +} + func TestVerifyLocalInstall(t *testing.T) { t.Parallel() @@ -514,49 +928,16 @@ func TestProvenanceConversionsPreserveEveryField(t *testing.T) { assert.Nil(t, provenanceInfoToLock(nil)) } -func TestProvenanceInfoFromCatalog(t *testing.T) { +func TestNormalizeCatalogProvenance(t *testing.T) { t.Parallel() - t.Run("nil is nil", func(t *testing.T) { - t.Parallel() - info, err := provenanceInfoFromCatalog(nil) - require.NoError(t, err) - assert.Nil(t, info) - }) + assert.Nil(t, normalizeCatalogProvenance(nil)) + assert.Nil(t, normalizeCatalogProvenance(®types.Provenance{}), + "an empty catalog block must preserve unconstrained TOFU behavior") - t.Run("every identity field carries through", func(t *testing.T) { - t.Parallel() - catalog := ®types.Provenance{ - SignerIdentity: testSignerIdentity, - CertIssuer: testCertIssuer, - RepositoryURI: "https://github.com/org/repo", - RepositoryRef: "refs/heads/main", - RunnerEnvironment: testRunnerEnvironment, - SigstoreURL: "https://rekor.sigstore.dev", - } - info, err := provenanceInfoFromCatalog(catalog) - require.NoError(t, err) - require.NotNil(t, info) - assert.Equal(t, catalog.SignerIdentity, info.SignerIdentity) - assert.Equal(t, catalog.CertIssuer, info.CertIssuer) - assert.Equal(t, catalog.RepositoryURI, info.RepositoryURI) - assert.Equal(t, catalog.RepositoryRef, info.RepositoryRef) - assert.Equal(t, catalog.RunnerEnvironment, info.RunnerEnvironment) - assert.Equal(t, catalog.SigstoreURL, info.SigstoreURL) - assert.False(t, info.Provisional, "a catalog-declared expectation is not itself a verified result") - }) - - t.Run("attestation constraint is refused, not silently dropped", func(t *testing.T) { - t.Parallel() - catalog := ®types.Provenance{ - SignerIdentity: testSignerIdentity, - Attestation: ®types.VerifiedAttestation{PredicateType: "https://slsa.dev/provenance/v1"}, - } - _, err := provenanceInfoFromCatalog(catalog) - require.Error(t, err) - assert.Equal(t, http.StatusUnprocessableEntity, httperr.Code(err)) - assert.Contains(t, err.Error(), "attestation") - }) + partial := ®types.Provenance{RepositoryRef: "refs/tags/v1.0.0"} + assert.Same(t, partial, normalizeCatalogProvenance(partial), + "a single supported constraint must not be discarded") } // requireAllFieldsSet fails when any field of the struct pointed to by v holds diff --git a/pkg/skills/verifier/git.go b/pkg/skills/verifier/git.go index a396abc5d6..bdb34f34c5 100644 --- a/pkg/skills/verifier/git.go +++ b/pkg/skills/verifier/git.go @@ -37,7 +37,7 @@ import ( func (*Default) VerifyGit( ctx context.Context, payload, signature []byte, - expected *lockfile.Provenance, + expected *ProvenanceExpectation, ) (*Result, error) { if len(signature) == 0 { return nil, fmt.Errorf("%w: commit is not signed", ErrUnsigned) @@ -59,14 +59,8 @@ func (*Default) VerifyGit( if err != nil { return nil, fmt.Errorf("%w: %w", ErrSignatureInvalid, err) } - if expected != nil { - if !gitIdentityMatches(observed.Identity, expected) { - return nil, fmt.Errorf("%w: commit is signed by a different identity than %q", - ErrSignerMismatch, expected.SignerIdentity) - } - if err := checkPinnedCertificateFields(observed, expected); err != nil { - return nil, err - } + if err := checkProvenanceExpectation(observed, expected); err != nil { + return nil, err } result := resultFromCore(observed, nil) // No transparency-log proof is validated yet (see the doc comment), so diff --git a/pkg/skills/verifier/git_test.go b/pkg/skills/verifier/git_test.go index 88ba26e153..1eb3752af4 100644 --- a/pkg/skills/verifier/git_test.go +++ b/pkg/skills/verifier/git_test.go @@ -331,9 +331,10 @@ func TestGitCertificateRefAndRunner(t *testing.T) { "the observed ref must reach the lock file, or nothing can be enforced later") assert.Equal(t, testGitRunner, provenance.RunnerEnvironment) - require.NoError(t, checkPinnedCertificateFields(observed, provenance)) + require.NoError(t, checkProvenanceExpectation(observed, NewLockExpectation(provenance))) require.ErrorIs(t, - checkPinnedCertificateFields(observed, &lockfile.Provenance{RepositoryRef: "refs/heads/attacker"}), + checkProvenanceExpectation( + observed, NewLockExpectation(&lockfile.Provenance{RepositoryRef: "refs/heads/attacker"})), ErrSignerMismatch) }) @@ -348,9 +349,9 @@ func TestGitCertificateRefAndRunner(t *testing.T) { require.NoError(t, err) assert.Empty(t, observed.RepositoryRef) assert.Empty(t, observed.RunnerEnvironment) - require.NoError(t, checkPinnedCertificateFields(observed, &lockfile.Provenance{ + require.NoError(t, checkProvenanceExpectation(observed, NewLockExpectation(&lockfile.Provenance{ SignerIdentity: "dev@example.com", CertIssuer: testGitIssuer, - }), "an entry pinning neither field must keep verifying") + })), "an entry pinning neither field must keep verifying") }) } diff --git a/pkg/skills/verifier/mocks/mock_verifier.go b/pkg/skills/verifier/mocks/mock_verifier.go index ee8287fda0..59855ce3f0 100644 --- a/pkg/skills/verifier/mocks/mock_verifier.go +++ b/pkg/skills/verifier/mocks/mock_verifier.go @@ -86,7 +86,7 @@ func (mr *MockVerifierMockRecorder) VerifyBundleOfflineWithKey(bundle, imageRef, } // VerifyGit mocks base method. -func (m *MockVerifier) VerifyGit(ctx context.Context, payload, signature []byte, expected *lockfile.Provenance) (*verifier.Result, error) { +func (m *MockVerifier) VerifyGit(ctx context.Context, payload, signature []byte, expected *verifier.ProvenanceExpectation) (*verifier.Result, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "VerifyGit", ctx, payload, signature, expected) ret0, _ := ret[0].(*verifier.Result) @@ -101,7 +101,7 @@ func (mr *MockVerifierMockRecorder) VerifyGit(ctx, payload, signature, expected } // VerifyOCI mocks base method. -func (m *MockVerifier) VerifyOCI(ctx context.Context, imageRef, digest string, expected *lockfile.Provenance) (*verifier.Result, error) { +func (m *MockVerifier) VerifyOCI(ctx context.Context, imageRef, digest string, expected *verifier.ProvenanceExpectation) (*verifier.Result, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "VerifyOCI", ctx, imageRef, digest, expected) ret0, _ := ret[0].(*verifier.Result) diff --git a/pkg/skills/verifier/oci.go b/pkg/skills/verifier/oci.go index e19c265595..ed0e1b4954 100644 --- a/pkg/skills/verifier/oci.go +++ b/pkg/skills/verifier/oci.go @@ -13,7 +13,6 @@ import ( "github.com/sigstore/sigstore-go/pkg/verify" coreverifier "github.com/stacklok/toolhive-core/container/verifier" - "github.com/stacklok/toolhive/pkg/skills/lockfile" ) // VerifyOCI discovers and verifies the Sigstore signature for an OCI @@ -22,7 +21,7 @@ import ( func (d *Default) VerifyOCI( ctx context.Context, imageRef, digest string, - expected *lockfile.Provenance, + expected *ProvenanceExpectation, ) (*Result, error) { bundles, err := d.retrieveBundles(ctx, imageRef, digest) if err != nil { @@ -69,36 +68,49 @@ func (d *Default) VerifyOCIWithKey( } // verifyKeylessBundles verifies bundles until one passes the keyless policy -// AND the pinned certificate fields the policy cannot express, returning its -// result, or nil with the most useful verification error. +// and its source-specific provenance expectation, returning its result or +// nil with the most useful verification error. func verifyKeylessBundles( bundles []coreverifier.Bundle, tm root.TrustedMaterial, opts []verify.VerifierOption, - expected *lockfile.Provenance, + expected *ProvenanceExpectation, ) (*Result, error) { - var errs []error - for _, b := range bundles { - result, err := verifyOneKeylessBundle(b, tm, opts, expected) + result, errs := firstVerifiedBundle(bundles, func(b coreverifier.Bundle) (*Result, error) { + return verifyOneKeylessBundle(b, tm, opts, expected) + }) + if result != nil { + return result, nil + } + return nil, mostUsefulVerifyError(errs) +} + +// firstVerifiedBundle returns the first bundle accepted by verify and keeps +// every rejection when none match. Keeping selection separate from the +// cryptographic operation makes the "any satisfying valid signature wins" +// rule explicit for multi-signature artifacts. +func firstVerifiedBundle[T any](bundles []T, verify func(T) (*Result, error)) (*Result, []error) { + errList := make([]error, 0, len(bundles)) + for _, bundle := range bundles { + result, err := verify(bundle) if err != nil { - errs = append(errs, err) + errList = append(errList, err) continue } return result, nil } - return nil, mostUsefulVerifyError(errs) + return nil, errList } // verifyOneKeylessBundle verifies a single bundle against the keyless -// policy, then the pinned certificate fields the policy cannot express. A -// pinned-field mismatch disqualifies the bundle exactly like a policy -// failure — another bundle on the artifact may satisfy the full -// expectation. +// policy, then its source-specific provenance expectation. A mismatch +// disqualifies the bundle exactly like a policy failure — another signature +// on the artifact may satisfy the full expectation. func verifyOneKeylessBundle( b coreverifier.Bundle, tm root.TrustedMaterial, opts []verify.VerifierOption, - expected *lockfile.Provenance, + expected *ProvenanceExpectation, ) (*Result, error) { vr, verifyErr := coreverifier.VerifyBundle(b, tm, expectedIdentity(expected), opts...) if verifyErr != nil { @@ -108,10 +120,9 @@ func verifyOneKeylessBundle( if idErr != nil { return nil, idErr } - // The Sigstore policy accepted this certificate's signer identity and - // issuer; the pinned ref and runner class are enforced against the same - // certificate here. - if err := checkPinnedCertificateFields(observed, expected); err != nil { + // Catalog constraints are checked here, inside the per-bundle loop, so a + // mismatching valid signature does not hide a later satisfying one. + if err := checkProvenanceExpectation(observed, expected); err != nil { return nil, err } return resultFromCore(observed, b.Raw), nil @@ -179,17 +190,15 @@ func splitEmbeddedDigest(imageRef string) (string, bool) { } // classifyVerifyFailure distinguishes a signer mismatch from an invalid -// signature. The expected identity is enforced inside the Sigstore policy, -// so a mismatch surfaces as a verification failure; re-verifying without -// the identity constraint tells the two apart: if the chain of trust holds -// without the constraint, the failure was the identity — and the identity -// that DID verify is reported, so an operator can tell a legitimate -// publisher rotation from an artifact substitution. +// signature. Lock identities are enforced inside the Sigstore policy, so a +// mismatch surfaces as a verification failure; re-verifying without the +// identity constraint tells the two apart. Catalog mismatches are already +// classified inside the per-bundle loop. func classifyVerifyFailure( bundles []coreverifier.Bundle, tm root.TrustedMaterial, opts []verify.VerifierOption, - expected *lockfile.Provenance, + expected *ProvenanceExpectation, lastErr error, ) error { // A pinned ref or runner mismatch is already the precise diagnosis, and @@ -214,15 +223,18 @@ func classifyVerifyFailure( // signerMismatchError builds the ErrSignerMismatch error, naming both the // expected identity tuple and the identity the artifact actually verifies // with (when extractable). -func signerMismatchError(vr *verify.VerificationResult, expected *lockfile.Provenance) error { - observed, idErr := coreverifier.IdentityFromResult(vr) +func signerMismatchError(vr *verify.VerificationResult, expected *ProvenanceExpectation) error { + observed, idErr := observedFromResult(vr) if idErr != nil { - return fmt.Errorf("%w: artifact is signed by a different identity than %q (issuer %q)", - ErrSignerMismatch, expected.SignerIdentity, expected.CertIssuer) + return fmt.Errorf("%w: artifact is signed by a different identity", ErrSignerMismatch) + } + if expected.catalog != nil { + return checkCatalogExpectation(observed, expected.catalog) } + p := expected.locked return fmt.Errorf("%w: locked to %q (issuer %q), but the artifact verifies as %q (issuer %q)", ErrSignerMismatch, - expected.SignerIdentity, expected.CertIssuer, + p.SignerIdentity, p.CertIssuer, observed.SignerIdentity, observed.CertIssuer) } diff --git a/pkg/skills/verifier/offline.go b/pkg/skills/verifier/offline.go index d804fcd548..ef4ceb406b 100644 --- a/pkg/skills/verifier/offline.go +++ b/pkg/skills/verifier/offline.go @@ -26,7 +26,7 @@ func (*Default) VerifyBundleOffline(bundleBytes []byte, digest string, expected // reinstalling (or re-adopting) is the fix. return fmt.Errorf("%w: no stored bundle to verify — reinstall to restore it", ErrSignatureInvalid) } - vr, err := coreverifier.VerifyBundleOffline(bundleBytes, digest, expectedIdentity(expected)) + vr, err := coreverifier.VerifyBundleOffline(bundleBytes, digest, expectedIdentity(NewLockExpectation(expected))) if err == nil { return checkStoredBundlePins(vr, expected) } @@ -40,7 +40,7 @@ func (*Default) VerifyBundleOffline(bundleBytes []byte, digest string, expected // mismatch apart from a broken signature — and yields the identity // that DID verify, which the error reports. if vr, tofuErr := coreverifier.VerifyBundleOffline(bundleBytes, digest, nil); tofuErr == nil { - return signerMismatchError(vr, expected) + return signerMismatchError(vr, NewLockExpectation(expected)) } } return wrapInvalid(err) diff --git a/pkg/skills/verifier/types.go b/pkg/skills/verifier/types.go index 85b6489742..10735119a5 100644 --- a/pkg/skills/verifier/types.go +++ b/pkg/skills/verifier/types.go @@ -10,6 +10,7 @@ import ( "github.com/sigstore/sigstore-go/pkg/verify" coreverifier "github.com/stacklok/toolhive-core/container/verifier" + regtypes "github.com/stacklok/toolhive-core/registry/types" "github.com/stacklok/toolhive/pkg/skills/lockfile" ) @@ -49,6 +50,30 @@ type Result struct { Bundle []byte } +// ProvenanceExpectation identifies both the provenance constraints and their +// source. Lock entries use their existing strict identity semantics, while +// catalog fields constrain independently and ignore empty values. +type ProvenanceExpectation struct { + locked *lockfile.Provenance + catalog *regtypes.Provenance +} + +// NewLockExpectation returns the strict expectation recorded in a lock file. +func NewLockExpectation(p *lockfile.Provenance) *ProvenanceExpectation { + if p == nil { + return nil + } + return &ProvenanceExpectation{locked: p} +} + +// NewCatalogExpectation returns independently optional catalog constraints. +func NewCatalogExpectation(p *regtypes.Provenance) *ProvenanceExpectation { + if p == nil { + return nil + } + return &ProvenanceExpectation{catalog: p} +} + // ToLockProvenance converts a verification result to a lock file provenance // block. Key-signed results have no certificate identity and yield nil — // the lock file records provenance only for identity-bearing signatures. @@ -67,19 +92,28 @@ func (r *Result) ToLockProvenance() *lockfile.Provenance { } } -// expectedIdentity converts a lock provenance block into the core Identity -// that gets bound into the Sigstore verification policy. A nil provenance -// (trust on first use) yields nil, which core treats as chain-of-trust-only +// expectedIdentity converts a lock expectation into the core Identity bound +// into the Sigstore verification policy. Catalog expectations and nil (trust +// on first use) yield nil, which core treats as chain-of-trust-only // verification. // // The recorded RepositoryRef and RunnerEnvironment are deliberately absent: // core's Identity cannot express them (and its SAN policy matches any ref), // so they are enforced against the certificate after the policy has passed — // see checkPinnedCertificateFields. -func expectedIdentity(p *lockfile.Provenance) *coreverifier.Identity { - if p == nil { +func expectedIdentity(expected *ProvenanceExpectation) *coreverifier.Identity { + if expected == nil { return nil } + if expected.catalog != nil { + // Catalog fields constrain independently. Core's identity policy + // composes repository URI and signer into a GitHub Actions SAN, which + // would cross-couple those fields and reject valid non-GitHub + // identities. Catalog matching therefore happens against the observed + // certificate after chain verification, inside the per-bundle loop. + return nil + } + p := expected.locked return &coreverifier.Identity{ SignerIdentity: p.SignerIdentity, CertIssuer: p.CertIssuer, @@ -139,16 +173,26 @@ func observedFromResult(vr *verify.VerificationResult) (observedCertificate, err }, nil } -// checkPinnedCertificateFields enforces the certificate fields the lock file -// pins that no Sigstore policy can express: the git ref the signing workflow -// ran on and the runner class it executed in. Without this, provenance -// recorded for "this workflow in this repository" is satisfied by the same -// workflow run from any branch or on a self-hosted runner. -// -// An empty expected field is unconstrained, not a wildcard mismatch: every -// lock entry written before these fields were recorded has them empty, and -// certificates from signers outside GitHub Actions carry no such extensions -// at all. Both must keep verifying. +// checkProvenanceExpectation applies the source-specific comparison after a +// certificate chain has verified. Lock identity semantics remain strict; +// catalog fields constrain independently and ignore empty values. +func checkProvenanceExpectation(observed observedCertificate, expected *ProvenanceExpectation) error { + if expected == nil { + return nil + } + if expected.catalog != nil { + return checkCatalogExpectation(observed, expected.catalog) + } + p := expected.locked + if !gitIdentityMatches(observed.Identity, p) { + return fmt.Errorf("%w: artifact is signed by a different identity than %q", + ErrSignerMismatch, p.SignerIdentity) + } + return checkPinnedCertificateFields(observed, p) +} + +// checkPinnedCertificateFields enforces lock fields the core Sigstore policy +// cannot express while retaining the lock file's established semantics. func checkPinnedCertificateFields(observed observedCertificate, expected *lockfile.Provenance) error { if expected == nil { return nil @@ -162,6 +206,27 @@ func checkPinnedCertificateFields(observed observedCertificate, expected *lockfi return nil } +func checkCatalogExpectation(observed observedCertificate, expected *regtypes.Provenance) error { + checks := []struct { + name string + expected string + observed string + }{ + {name: "signer identity", expected: expected.SignerIdentity, observed: observed.SignerIdentity}, + {name: "certificate issuer", expected: expected.CertIssuer, observed: observed.CertIssuer}, + {name: "repository URI", expected: expected.RepositoryURI, observed: observed.SourceRepositoryURI}, + {name: "repository ref", expected: expected.RepositoryRef, observed: observed.RepositoryRef}, + {name: "runner environment", expected: expected.RunnerEnvironment, observed: observed.RunnerEnvironment}, + } + for _, check := range checks { + if check.expected != "" && check.observed != check.expected { + return fmt.Errorf("%w: catalog requires %s %q, but the artifact carries %s", + ErrSignerMismatch, check.name, check.expected, quotedOrNone(check.observed)) + } + } + return nil +} + // pinnedFieldMismatch builds the ErrSignerMismatch error for a certificate // field that differs from the pinned one, naming the field so the operator // can tell a ref change from a runner-class change. diff --git a/pkg/skills/verifier/verifier.go b/pkg/skills/verifier/verifier.go index 0463d33472..acbc442664 100644 --- a/pkg/skills/verifier/verifier.go +++ b/pkg/skills/verifier/verifier.go @@ -3,9 +3,10 @@ // Package verifier verifies Sigstore signatures on skill artifacts. // -// It is a thin wrapper over toolhive-core's container/verifier exports: all -// cryptographic verification — including binding an expected identity into -// the Sigstore policy — happens in core. This package adds the +// It is a thin wrapper over toolhive-core's container/verifier exports. Lock +// identities are bound into core's Sigstore policy; independently optional +// catalog constraints are matched against the observed certificate inside +// each bundle-verification attempt. This package adds the // skills-domain vocabulary: lock file provenance conversion, the // unsigned/invalid/mismatch error taxonomy, and the trust-on-first-use flow // (nil expected identity verifies the chain of trust only; the caller @@ -34,12 +35,12 @@ import ( type Verifier interface { // VerifyOCI discovers the Sigstore signature material attached to the // OCI artifact and verifies it (keyless/Fulcio flow). A non-nil - // expected identity is enforced inside the Sigstore verification - // policy, and its recorded repository ref and runner environment - // against the verified certificate; nil expected is the - // trust-on-first-use case and verifies the chain of trust only. + // lock expectation is enforced inside the Sigstore verification policy; + // catalog fields are checked independently against each verified bundle. + // nil expected is the trust-on-first-use case and verifies the chain of + // trust only. // Returns ErrUnsigned when the artifact carries no signature material. - VerifyOCI(ctx context.Context, imageRef, digest string, expected *lockfile.Provenance) (*Result, error) + VerifyOCI(ctx context.Context, imageRef, digest string, expected *ProvenanceExpectation) (*Result, error) // VerifyOCIWithKey discovers the signature material and verifies it // against the given PEM public key (the cosign key-pair flow). @@ -48,10 +49,10 @@ type Verifier interface { // VerifyGit cryptographically verifies a gitsign commit signature over // the commit payload against the embedded Fulcio roots. A non-nil - // expected identity must match the certificate identity; nil expected - // is the trust-on-first-use case. Returns ErrUnsigned for an empty - // signature. - VerifyGit(ctx context.Context, payload, signature []byte, expected *lockfile.Provenance) (*Result, error) + // expectation must match the certificate identity according to its lock + // or catalog semantics; nil expected is the trust-on-first-use case. + // Returns ErrUnsigned for an empty signature. + VerifyGit(ctx context.Context, payload, signature []byte, expected *ProvenanceExpectation) (*Result, error) // VerifyBundleOffline re-verifies a stored bundle against the artifact // digest ("sha256:") without network access, enforcing expected diff --git a/pkg/skills/verifier/verifier_test.go b/pkg/skills/verifier/verifier_test.go index a7c5a69be6..817ba5269a 100644 --- a/pkg/skills/verifier/verifier_test.go +++ b/pkg/skills/verifier/verifier_test.go @@ -8,6 +8,7 @@ import ( "crypto/elliptic" "crypto/rand" "errors" + "fmt" "net/http/httptest" "os" "path/filepath" @@ -24,6 +25,7 @@ import ( "github.com/stacklok/toolhive-core/container/signer" coreverifier "github.com/stacklok/toolhive-core/container/verifier" + regtypes "github.com/stacklok/toolhive-core/registry/types" "github.com/stacklok/toolhive/pkg/skills/lockfile" ) @@ -272,20 +274,31 @@ func TestExpectedIdentityConversion(t *testing.T) { assert.Nil(t, expectedIdentity(nil), "TOFU first use must yield a nil core identity") - got := expectedIdentity(&lockfile.Provenance{ + got := expectedIdentity(NewLockExpectation(&lockfile.Provenance{ SignerIdentity: "/.github/workflows/release.yml", CertIssuer: "https://token.actions.githubusercontent.com", RepositoryURI: "https://github.com/org/repo", RepositoryRef: "refs/tags/v0.1.0", RunnerEnvironment: "github-hosted", - }) + })) require.NotNil(t, got) assert.Equal(t, "/.github/workflows/release.yml", got.SignerIdentity) assert.Equal(t, "https://token.actions.githubusercontent.com", got.CertIssuer) assert.Equal(t, "https://github.com/org/repo", got.SourceRepositoryURI) + + assert.Nil(t, expectedIdentity(NewCatalogExpectation(®types.Provenance{ + SignerIdentity: "/.github/workflows/release.yml", + CertIssuer: "https://token.actions.githubusercontent.com", + })), "signer and issuer without repository must not be bound into core's composed GitHub policy") + + assert.Nil(t, expectedIdentity(NewCatalogExpectation(®types.Provenance{ + SignerIdentity: "/.github/workflows/release.yml", + CertIssuer: "https://token.actions.githubusercontent.com", + RepositoryURI: "https://github.com/org/repo", + })), "even a complete catalog identity must retain independent-field semantics") } -func TestCheckPinnedCertificateFields(t *testing.T) { +func TestCheckLockProvenanceExpectation(t *testing.T) { t.Parallel() observed := observedCertificate{ @@ -340,7 +353,7 @@ func TestCheckPinnedCertificateFields(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { t.Parallel() - err := checkPinnedCertificateFields(tc.observed, tc.expected) + err := checkProvenanceExpectation(tc.observed, NewLockExpectation(tc.expected)) if !tc.wantErr { require.NoError(t, err) return @@ -352,6 +365,48 @@ func TestCheckPinnedCertificateFields(t *testing.T) { } } +func TestCheckCatalogProvenanceExpectation(t *testing.T) { + t.Parallel() + + observed := observedCertificate{ + Identity: coreverifier.Identity{ + SignerIdentity: "/.github/workflows/release.yml", + CertIssuer: "https://token.actions.githubusercontent.com", + SourceRepositoryURI: "https://github.com/org/repo", + }, + RepositoryRef: "refs/tags/v1.0.0", + RunnerEnvironment: "github-hosted", + } + tests := []struct { + name string + expected *regtypes.Provenance + wantErr bool + }{ + {name: "empty constraints", expected: ®types.Provenance{}}, + {name: "signer only", expected: ®types.Provenance{SignerIdentity: observed.SignerIdentity}}, + {name: "issuer only", expected: ®types.Provenance{CertIssuer: observed.CertIssuer}}, + {name: "repository only", expected: ®types.Provenance{RepositoryURI: observed.SourceRepositoryURI}}, + {name: "ref only", expected: ®types.Provenance{RepositoryRef: observed.RepositoryRef}}, + {name: "runner only", expected: ®types.Provenance{RunnerEnvironment: observed.RunnerEnvironment}}, + {name: "signer mismatch", expected: ®types.Provenance{SignerIdentity: "attacker@example.com"}, wantErr: true}, + {name: "issuer mismatch", expected: ®types.Provenance{CertIssuer: "https://issuer.example.com"}, wantErr: true}, + {name: "repository mismatch", expected: ®types.Provenance{RepositoryURI: "https://github.com/attacker/repo"}, wantErr: true}, + {name: "ref mismatch", expected: ®types.Provenance{RepositoryRef: "refs/heads/attacker"}, wantErr: true}, + {name: "runner mismatch", expected: ®types.Provenance{RunnerEnvironment: "self-hosted"}, wantErr: true}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + err := checkProvenanceExpectation(observed, NewCatalogExpectation(tc.expected)) + if tc.wantErr { + require.ErrorIs(t, err, ErrSignerMismatch) + return + } + require.NoError(t, err) + }) + } +} + // TestClassifyVerifyFailureKeepsPinnedFieldDiagnosis guards the interaction // between the two mismatch sources: a pinned ref/runner failure is reported // verbatim instead of being re-derived as a signer-identity mismatch, which @@ -365,7 +420,7 @@ func TestClassifyVerifyFailureKeepsPinnedFieldDiagnosis(t *testing.T) { } pinErr := pinnedFieldMismatch("repository ref", expected.RepositoryRef, "refs/heads/main") - err := classifyVerifyFailure(nil, nil, nil, expected, pinErr) + err := classifyVerifyFailure(nil, nil, nil, NewLockExpectation(expected), pinErr) require.ErrorIs(t, err, ErrSignerMismatch) assert.Contains(t, err.Error(), "repository ref") assert.NotErrorIs(t, err, ErrSignatureInvalid) @@ -420,3 +475,41 @@ func TestMostUsefulVerifyErrorPrefersPinnedFieldMismatch(t *testing.T) { }) } } + +func TestFirstVerifiedBundleSelectsLaterCatalogMatch(t *testing.T) { + t.Parallel() + + want := &Result{Signed: true, SignerIdentity: "matching@example.com"} + visited := make([]string, 0, 3) + result, errs := firstVerifiedBundle([]string{"valid-mismatch", "valid-match", "unused"}, + func(candidate string) (*Result, error) { + visited = append(visited, candidate) + switch candidate { + case "valid-mismatch": + return nil, fmt.Errorf("%w: first valid signature does not satisfy the catalog", ErrSignerMismatch) + case "valid-match": + return want, nil + default: + t.Fatalf("selection must stop after the later matching signature") + return nil, nil + } + }) + + assert.Same(t, want, result) + assert.Nil(t, errs) + assert.Equal(t, []string{"valid-mismatch", "valid-match"}, visited, + "a mismatching valid signature must not hide a later catalog match") +} + +func TestFirstVerifiedBundleReturnsEveryRejection(t *testing.T) { + t.Parallel() + + result, errs := firstVerifiedBundle([]string{"first", "second"}, func(candidate string) (*Result, error) { + return nil, errors.New(candidate) + }) + + assert.Nil(t, result) + require.Len(t, errs, 2) + assert.EqualError(t, errs[0], "first") + assert.EqualError(t, errs[1], "second") +} From 772b43f838d370bf99e638b554e654140368a7dd Mon Sep 17 00:00:00 2001 From: Samuele Verzi Date: Mon, 24 Aug 2026 19:54:11 +0200 Subject: [PATCH 5/6] Fix provenance test fixtures Signed-off-by: Samuele Verzi --- pkg/skills/skillsvc/verify_test.go | 5 +++- pkg/skills/verifier/verifier_test.go | 36 ++++++++++++++++++++-------- 2 files changed, 30 insertions(+), 11 deletions(-) diff --git a/pkg/skills/skillsvc/verify_test.go b/pkg/skills/skillsvc/verify_test.go index 0a3cc1ee0c..b75339bfe6 100644 --- a/pkg/skills/skillsvc/verify_test.go +++ b/pkg/skills/skillsvc/verify_test.go @@ -716,6 +716,9 @@ func TestInstallVerification_EnforcesPinnedRef(t *testing.T) { } _, err := svc.Install(t.Context(), installOpts) require.NoError(t, err) + entry, ok := loadLockEntry(t, projectRoot, "ref-pinned-skill") + require.True(t, ok) + require.NotNil(t, entry.Provenance) mv.EXPECT().VerifyGit(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). DoAndReturn(func(_ any, _, _ []byte, expected *verifier.ProvenanceExpectation) (*verifier.Result, error) { @@ -729,7 +732,7 @@ func TestInstallVerification_EnforcesPinnedRef(t *testing.T) { require.Error(t, err) assert.Equal(t, http.StatusForbidden, httperr.Code(err)) - entry, ok := loadLockEntry(t, projectRoot, "ref-pinned-skill") + entry, ok = loadLockEntry(t, projectRoot, "ref-pinned-skill") require.True(t, ok) require.NotNil(t, entry.Provenance) assert.Equal(t, "refs/tags/v0.1.0", entry.Provenance.RepositoryRef, "the rejected install must not re-pin") diff --git a/pkg/skills/verifier/verifier_test.go b/pkg/skills/verifier/verifier_test.go index 817ba5269a..e640a04917 100644 --- a/pkg/skills/verifier/verifier_test.go +++ b/pkg/skills/verifier/verifier_test.go @@ -326,26 +326,42 @@ func TestCheckLockProvenanceExpectation(t *testing.T) { { name: "both fields match", observed: observed, - expected: &lockfile.Provenance{RepositoryRef: "refs/tags/v0.1.0", RunnerEnvironment: "github-hosted"}, + expected: &lockfile.Provenance{ + SignerIdentity: "/.github/workflows/release.yml", + RepositoryRef: "refs/tags/v0.1.0", + RunnerEnvironment: "github-hosted", + }, }, { - name: "different ref rejected", - observed: observed, - expected: &lockfile.Provenance{RepositoryRef: "refs/heads/attacker-branch"}, + name: "different ref rejected", + observed: observed, + expected: &lockfile.Provenance{ + SignerIdentity: "/.github/workflows/release.yml", + RepositoryRef: "refs/heads/attacker-branch", + }, wantErr: true, wantMessage: "repository ref", }, { - name: "different runner class rejected", - observed: observed, - expected: &lockfile.Provenance{RunnerEnvironment: "self-hosted"}, + name: "different runner class rejected", + observed: observed, + expected: &lockfile.Provenance{ + SignerIdentity: "/.github/workflows/release.yml", + RunnerEnvironment: "self-hosted", + }, wantErr: true, wantMessage: "runner environment", }, { - name: "certificate that stopped carrying the pinned ref rejected", - observed: observedCertificate{RunnerEnvironment: "github-hosted"}, - expected: &lockfile.Provenance{RepositoryRef: "refs/tags/v0.1.0"}, + name: "certificate that stopped carrying the pinned ref rejected", + observed: observedCertificate{ + Identity: coreverifier.Identity{SignerIdentity: "/.github/workflows/release.yml"}, + RunnerEnvironment: "github-hosted", + }, + expected: &lockfile.Provenance{ + SignerIdentity: "/.github/workflows/release.yml", + RepositoryRef: "refs/tags/v0.1.0", + }, wantErr: true, wantMessage: "carries none", }, From d4edb3108b070da33e8aaafb1c7b1e3a070e6608 Mon Sep 17 00:00:00 2001 From: Samuele Verzi Date: Mon, 24 Aug 2026 20:00:15 +0200 Subject: [PATCH 6/6] Satisfy provenance lint checks Signed-off-by: Samuele Verzi --- pkg/skills/skillsvc/verify_test.go | 30 ++++++++++++++---------------- pkg/skills/verifier/oci.go | 4 ++-- 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/pkg/skills/skillsvc/verify_test.go b/pkg/skills/skillsvc/verify_test.go index b75339bfe6..cdba3bf5a1 100644 --- a/pkg/skills/skillsvc/verify_test.go +++ b/pkg/skills/skillsvc/verify_test.go @@ -338,24 +338,23 @@ func TestVerifyInstall_CatalogIdentityPairPreservesCatalogSemantics(t *testing.T CertIssuer: testCertIssuer, }, } - checkExpected := func(expected *verifier.ProvenanceExpectation) (*verifier.Result, error) { + checkExpected := func(expected *verifier.ProvenanceExpectation) { require.NotNil(t, expected) assert.Equal(t, verifier.NewCatalogExpectation(opts.CatalogProvenance), expected) - return signedResult(), nil } var err error if backend == "git" { mv.EXPECT().VerifyGit(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). - DoAndReturn(func(_ any, _, _ []byte, expected *verifier.ProvenanceExpectation) (*verifier.Result, error) { - return checkExpected(expected) - }) + Do(func(_ any, _, _ []byte, expected *verifier.ProvenanceExpectation) { + checkExpected(expected) + }).Return(signedResult(), nil) _, err = svc.verifyGitInstall(t.Context(), opts, "catalog-skill", []byte("payload"), "signature") } else { mv.EXPECT().VerifyOCI(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). - DoAndReturn(func(_ any, _, _ string, expected *verifier.ProvenanceExpectation) (*verifier.Result, error) { - return checkExpected(expected) - }) + Do(func(_ any, _, _ string, expected *verifier.ProvenanceExpectation) { + checkExpected(expected) + }).Return(signedResult(), nil) _, err = svc.verifyOCIInstall( t.Context(), opts, "catalog-skill", "ghcr.io/test/catalog-skill:v1", "sha256:digest") } @@ -497,7 +496,7 @@ func TestVerifyInstall_UnsupportedCatalogConstraintsOnlyFailOnFirstUse(t *testin } if !state.wantErr { - checkExpected := func(expected *verifier.ProvenanceExpectation) (*verifier.Result, error) { + checkExpected := func(expected *verifier.ProvenanceExpectation) { if state.wantExpected { require.NotNil(t, expected) assert.Equal(t, verifier.NewLockExpectation(state.entry.Provenance), expected) @@ -505,18 +504,17 @@ func TestVerifyInstall_UnsupportedCatalogConstraintsOnlyFailOnFirstUse(t *testin assert.Nil(t, expected, "a legacy lock must suppress catalog policy without inventing lock trust") } - return signedResult(), nil } if backend == "git" { mv.EXPECT().VerifyGit(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). - DoAndReturn(func(_ any, _, _ []byte, expected *verifier.ProvenanceExpectation) (*verifier.Result, error) { - return checkExpected(expected) - }) + Do(func(_ any, _, _ []byte, expected *verifier.ProvenanceExpectation) { + checkExpected(expected) + }).Return(signedResult(), nil) } else { mv.EXPECT().VerifyOCI(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). - DoAndReturn(func(_ any, _, _ string, expected *verifier.ProvenanceExpectation) (*verifier.Result, error) { - return checkExpected(expected) - }) + Do(func(_ any, _, _ string, expected *verifier.ProvenanceExpectation) { + checkExpected(expected) + }).Return(signedResult(), nil) } } diff --git a/pkg/skills/verifier/oci.go b/pkg/skills/verifier/oci.go index ed0e1b4954..91ab7ec1fa 100644 --- a/pkg/skills/verifier/oci.go +++ b/pkg/skills/verifier/oci.go @@ -89,10 +89,10 @@ func verifyKeylessBundles( // every rejection when none match. Keeping selection separate from the // cryptographic operation makes the "any satisfying valid signature wins" // rule explicit for multi-signature artifacts. -func firstVerifiedBundle[T any](bundles []T, verify func(T) (*Result, error)) (*Result, []error) { +func firstVerifiedBundle[T any](bundles []T, verifyBundle func(T) (*Result, error)) (*Result, []error) { errList := make([]error, 0, len(bundles)) for _, bundle := range bundles { - result, err := verify(bundle) + result, err := verifyBundle(bundle) if err != nil { errList = append(errList, err) continue