diff --git a/.github/workflows/build-pr.yml b/.github/workflows/build-pr.yml index 0b1d27334..1b598cba4 100644 --- a/.github/workflows/build-pr.yml +++ b/.github/workflows/build-pr.yml @@ -77,7 +77,7 @@ jobs: - name: Check out code into the Go module directory uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up Go 1.22 - uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0 + uses: actions/setup-go@3041bf56c941b39c61721a86cd11f3bb1338122a # v5.2.0 with: go-version: "1.22" diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index e88b04fc7..ee5f04a56 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -33,11 +33,11 @@ jobs: - name: Checkout repository uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # tag=3.0.2 - name: setup go environment - uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0 + uses: actions/setup-go@3041bf56c941b39c61721a86cd11f3bb1338122a # v5.2.0 with: go-version: "1.22" - name: Initialize CodeQL - uses: github/codeql-action/init@aa578102511db1f4524ed59b8cc2bae4f6e88195 # tag=v3.27.6 + uses: github/codeql-action/init@babb554ede22fd5605947329c4d04d8e7a0b8155 # tag=v3.27.7 with: languages: go - name: Run tidy @@ -45,4 +45,4 @@ jobs: - name: Build CLI run: make build - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@aa578102511db1f4524ed59b8cc2bae4f6e88195 # tag=v3.27.6 + uses: github/codeql-action/analyze@babb554ede22fd5605947329c4d04d8e7a0b8155 # tag=v3.27.7 diff --git a/.github/workflows/e2e-aks.yml b/.github/workflows/e2e-aks.yml index 1e1d674a5..03b9aeaf2 100644 --- a/.github/workflows/e2e-aks.yml +++ b/.github/workflows/e2e-aks.yml @@ -35,7 +35,7 @@ jobs: - name: Check out code into the Go module directory uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up Go 1.22 - uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0 + uses: actions/setup-go@3041bf56c941b39c61721a86cd11f3bb1338122a # v5.2.0 with: go-version: "1.22" - name: Az CLI login diff --git a/.github/workflows/e2e-cli.yml b/.github/workflows/e2e-cli.yml index bd12aab63..c301c58ee 100644 --- a/.github/workflows/e2e-cli.yml +++ b/.github/workflows/e2e-cli.yml @@ -41,7 +41,7 @@ jobs: - name: Checkout uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - name: setup go environment - uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0 + uses: actions/setup-go@3041bf56c941b39c61721a86cd11f3bb1338122a # v5.2.0 with: go-version: "1.22" - name: Run tidy @@ -51,7 +51,7 @@ jobs: - name: Check build run: bin/ratify version - name: Upload coverage to codecov.io - uses: codecov/codecov-action@015f24e6818733317a2da2edd6290ab26238649a # v5.0.7 + uses: codecov/codecov-action@7f8b4b4bde536c465e797be725718b88c5d95e0e # v5.1.1 with: token: ${{ secrets.CODECOV_TOKEN }} - name: Run helm lint @@ -70,7 +70,7 @@ jobs: - name: Checkout uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: setup go environment - uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0 + uses: actions/setup-go@3041bf56c941b39c61721a86cd11f3bb1338122a # v5.2.0 with: go-version: "1.22" - name: Run tidy @@ -86,7 +86,7 @@ jobs: make install ratify-config install-bats make test-e2e-cli GOCOVERDIR=${GITHUB_WORKSPACE}/test/e2e/.cover - name: Upload coverage to codecov.io - uses: codecov/codecov-action@015f24e6818733317a2da2edd6290ab26238649a # v5.0.7 + uses: codecov/codecov-action@7f8b4b4bde536c465e797be725718b88c5d95e0e # v5.1.1 with: token: ${{ secrets.CODECOV_TOKEN }} markdown-link-check: diff --git a/.github/workflows/e2e-k8s.yml b/.github/workflows/e2e-k8s.yml index 7bef51831..3dde3f42c 100644 --- a/.github/workflows/e2e-k8s.yml +++ b/.github/workflows/e2e-k8s.yml @@ -33,7 +33,7 @@ jobs: - name: Check out code into the Go module directory uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up Go 1.22 - uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0 + uses: actions/setup-go@3041bf56c941b39c61721a86cd11f3bb1338122a # v5.2.0 with: go-version: "1.22" - name: Restore Trivy cache diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index 54319e002..d74fea83b 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -19,7 +19,7 @@ jobs: with: egress-policy: audit - - uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0 + - uses: actions/setup-go@3041bf56c941b39c61721a86cd11f3bb1338122a # v5.2.0 with: go-version: "1.22" - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 diff --git a/.github/workflows/high-availability.yml b/.github/workflows/high-availability.yml index e64c6923f..5abb7cd75 100644 --- a/.github/workflows/high-availability.yml +++ b/.github/workflows/high-availability.yml @@ -37,7 +37,7 @@ jobs: - name: Check out code into the Go module directory uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up Go 1.22 - uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0 + uses: actions/setup-go@3041bf56c941b39c61721a86cd11f3bb1338122a # v5.2.0 with: go-version: "1.22" diff --git a/.github/workflows/quick-start.yml b/.github/workflows/quick-start.yml index 372faf83c..c8c224c64 100644 --- a/.github/workflows/quick-start.yml +++ b/.github/workflows/quick-start.yml @@ -37,7 +37,7 @@ jobs: - name: Checkout uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: setup go environment - uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0 + uses: actions/setup-go@3041bf56c941b39c61721a86cd11f3bb1338122a # v5.2.0 with: go-version: "1.22" - name: Run tidy diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b323cfe6f..ea24dfe31 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -29,7 +29,7 @@ jobs: uses: anchore/sbom-action/download-syft@55dc4ee22412511ee8c3142cbea40418e6cec693 # v0.17.8 - name: Set up Go - uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0 + uses: actions/setup-go@3041bf56c941b39c61721a86cd11f3bb1338122a # v5.2.0 with: go-version: "1.22" diff --git a/.github/workflows/run-full-validation.yml b/.github/workflows/run-full-validation.yml index 00e7bc661..bd48c5941 100644 --- a/.github/workflows/run-full-validation.yml +++ b/.github/workflows/run-full-validation.yml @@ -65,7 +65,7 @@ jobs: - name: Check out code into the Go module directory uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up Go 1.22 - uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0 + uses: actions/setup-go@3041bf56c941b39c61721a86cd11f3bb1338122a # v5.2.0 with: go-version: "1.22" diff --git a/.github/workflows/scan-vulns.yaml b/.github/workflows/scan-vulns.yaml index 3a9fbba85..1d0b85298 100644 --- a/.github/workflows/scan-vulns.yaml +++ b/.github/workflows/scan-vulns.yaml @@ -27,7 +27,7 @@ jobs: with: egress-policy: audit - - uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0 + - uses: actions/setup-go@3041bf56c941b39c61721a86cd11f3bb1338122a # v5.2.0 with: go-version: "1.22" check-latest: true diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index b18a16885..6d611c6c0 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -55,6 +55,6 @@ jobs: retention-days: 5 - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@aa578102511db1f4524ed59b8cc2bae4f6e88195 # tag=v3.27.6 + uses: github/codeql-action/upload-sarif@babb554ede22fd5605947329c4d04d8e7a0b8155 # tag=v3.27.7 with: sarif_file: results.sarif diff --git a/.github/workflows/update-trivy-cache.yml b/.github/workflows/update-trivy-cache.yml index 6d2fea0be..15e411b39 100644 --- a/.github/workflows/update-trivy-cache.yml +++ b/.github/workflows/update-trivy-cache.yml @@ -36,7 +36,7 @@ jobs: rm db.tar.gz - name: Cache DBs - uses: actions/cache/save@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2 + uses: actions/cache/save@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 with: path: ${{ github.workspace }}/.cache/trivy key: cache-trivy-${{ steps.date.outputs.date }} \ No newline at end of file diff --git a/RELEASES.md b/RELEASES.md index 1069f3f1f..28d6754ac 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -92,13 +92,13 @@ After a successful release, please prepare a [PR](https://github.com/ratify-proj * Contributors MUST select the `Helm Chart Change` option under the `Type of Change` section if there is ANY update to the helm chart that is required for proposed changes in PR. * Maintainers MUST manually trigger the "Publish Package" workflow after merging any PR that indicates `Helm Chart Change` * Go to the `Actions` tab for the Ratify repository - * Select `publish-ghcr` option from list of workflows on left pane + * Select `publish-dev-assets` option from list of workflows on left pane * Select the `Run workflow` drop down on the right side above the list of action runs - * Choose `Branch: main` + * Choose `Branch: dev` * Select `Run workflow` * Process to Request an off-schedule dev build be published * Submit a new feature request issue prefixed with `[Dev Build Request]` - * In the the `What this PR does / why we need it` section, briefly explain why an off schedule build is needed + * In the the `What would you like to be added?` section, briefly explain why an off schedule build is needed * Once issue is created, post in the `#ratify` slack channel and tag the maintainers * Maintainers should acknowledge request by approving/denying request as a follow up comment diff --git a/crd.Dockerfile b/crd.Dockerfile index 20e1c1b4b..d4578a4da 100644 --- a/crd.Dockerfile +++ b/crd.Dockerfile @@ -11,7 +11,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -FROM alpine@sha256:1e42bbe2508154c9126d48c2b8a75420c3544343bf86fd041fb7527e017a4b4a as builder +FROM alpine@sha256:21dc6063fd678b478f57c0e13f47560d0ea4eeba26dfc947b2a4f81f686b9f45 as builder ARG TARGETOS ARG TARGETARCH diff --git a/go.mod b/go.mod index 38a48896b..e09f9a4b4 100644 --- a/go.mod +++ b/go.mod @@ -36,7 +36,7 @@ require ( github.com/golang/protobuf v1.5.4 github.com/google/go-containerregistry v0.20.2 github.com/gorilla/mux v1.8.1 - github.com/notaryproject/notation-core-go v1.2.0-rc.1 + github.com/notaryproject/notation-core-go v1.2.0-rc.2 github.com/notaryproject/notation-go v1.3.0-rc.1 github.com/notaryproject/notation-plugin-framework-go v1.0.0 github.com/open-policy-agent/cert-controller v0.8.0 @@ -56,7 +56,7 @@ require ( go.opentelemetry.io/otel/metric v1.29.0 go.opentelemetry.io/otel/sdk/metric v1.27.0 golang.org/x/sync v0.9.0 - google.golang.org/grpc v1.68.0 + google.golang.org/grpc v1.68.1 google.golang.org/protobuf v1.35.2 k8s.io/api v0.28.15 k8s.io/apimachinery v0.28.15 @@ -114,7 +114,7 @@ require ( github.com/hashicorp/go-retryablehttp v0.7.7 // indirect github.com/miekg/pkcs11 v1.1.1 // indirect github.com/mozillazg/docker-credential-acr-helper v0.3.0 // indirect - github.com/notaryproject/tspclient-go v0.2.0 // indirect + github.com/notaryproject/tspclient-go v1.0.0-rc.1 // indirect github.com/nozzle/throttler v0.0.0-20180817012639-2ea982251481 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect diff --git a/go.sum b/go.sum index 118a71986..9befb9b7c 100644 --- a/go.sum +++ b/go.sum @@ -564,14 +564,14 @@ github.com/mozillazg/docker-credential-acr-helper v0.3.0/go.mod h1:cZlu3tof523uj github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= -github.com/notaryproject/notation-core-go v1.2.0-rc.1 h1:VMFlG+9a1JoNAQ3M96g8iqCq0cDRtE7XBaiTD8Ouvqw= -github.com/notaryproject/notation-core-go v1.2.0-rc.1/go.mod h1:b/70rA4OgOHlg0A7pb8zTWKJadFO6781zS3a37KHEJQ= +github.com/notaryproject/notation-core-go v1.2.0-rc.2 h1:0jOItalNwBNUhyuc5PPHQxO3jIZ5xRYq+IvRMQXNbuE= +github.com/notaryproject/notation-core-go v1.2.0-rc.2/go.mod h1:7aIcavfywFvBQoYyfVFJB501kt7Etqyubrt5mhJBG2c= github.com/notaryproject/notation-go v1.3.0-rc.1 h1:pm9tdUy2tWYqlwyRDZyKXgLwAscDATPUYv0ul2RK/Iw= github.com/notaryproject/notation-go v1.3.0-rc.1/go.mod h1:W4o45yolX4Q+3PKlcpGleLLXEKWHa3BshEqw/JX5c6I= github.com/notaryproject/notation-plugin-framework-go v1.0.0 h1:6Qzr7DGXoCgXEQN+1gTZWuJAZvxh3p8Lryjn5FaLzi4= github.com/notaryproject/notation-plugin-framework-go v1.0.0/go.mod h1:RqWSrTOtEASCrGOEffq0n8pSg2KOgKYiWqFWczRSics= -github.com/notaryproject/tspclient-go v0.2.0 h1:g/KpQGmyk/h7j60irIRG1mfWnibNOzJ8WhLqAzuiQAQ= -github.com/notaryproject/tspclient-go v0.2.0/go.mod h1:LGyA/6Kwd2FlM0uk8Vc5il3j0CddbWSHBj/4kxQDbjs= +github.com/notaryproject/tspclient-go v1.0.0-rc.1 h1:KcHxlqg6Adt4kzGLw012i0YMLlwGwToiR129c6IQ7Ys= +github.com/notaryproject/tspclient-go v1.0.0-rc.1/go.mod h1:LGyA/6Kwd2FlM0uk8Vc5il3j0CddbWSHBj/4kxQDbjs= github.com/nozzle/throttler v0.0.0-20180817012639-2ea982251481 h1:Up6+btDp321ZG5/zdSLo48H9Iaq0UQGthrhWC6pCxzE= github.com/nozzle/throttler v0.0.0-20180817012639-2ea982251481/go.mod h1:yKZQO8QE2bHlgozqWDiRVqTFlLQSj30K/6SAK8EeYFw= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= @@ -1002,8 +1002,8 @@ google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZi google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0= -google.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA= +google.golang.org/grpc v1.68.1 h1:oI5oTa11+ng8r8XMMN7jAOmWfPZWbYpCFaMUTACxkM0= +google.golang.org/grpc v1.68.1/go.mod h1:+q1XYFJjShcqn0QZHvCyeR4CXPA+llXIeUIfIe00waw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= diff --git a/pkg/keymanagementprovider/refresh/kubeRefresh.go b/pkg/keymanagementprovider/refresh/kubeRefresh.go index 895cb7d8d..cd296e0e2 100644 --- a/pkg/keymanagementprovider/refresh/kubeRefresh.go +++ b/pkg/keymanagementprovider/refresh/kubeRefresh.go @@ -24,6 +24,7 @@ import ( re "github.com/ratify-project/ratify/errors" kmp "github.com/ratify-project/ratify/pkg/keymanagementprovider" + nv "github.com/ratify-project/ratify/pkg/verifier/notation" "github.com/sirupsen/logrus" ctrl "sigs.k8s.io/controller-runtime" ) @@ -35,6 +36,7 @@ type KubeRefresher struct { Resource string Result ctrl.Result Status kmp.KeyManagementProviderStatus + CRLHandler nv.RevocationFactory } // Register registers the kubeRefresher factory @@ -54,6 +56,15 @@ func (kr *KubeRefresher) Refresh(ctx context.Context) error { return kmpErr } + // fetch CRLs and cache them + crlFetcher, err := kr.CRLHandler.NewFetcher() + if err != nil { + // log error and continue + logger.Warnf("Unable to create CRL fetcher for key management provider %s of type %s with error: %v", kr.Resource, kr.ProviderType, err) + } + for _, cert := range certificates { + nv.CacheCRL(ctx, cert, crlFetcher) + } // fetch keys and store in map keys, keyAttributes, err := kr.Provider.GetKeys(ctx) if err != nil { @@ -109,5 +120,6 @@ func (kr *KubeRefresher) Create(config RefresherConfig) (Refresher, error) { ProviderType: config.ProviderType, ProviderRefreshInterval: config.ProviderRefreshInterval, Resource: config.Resource, + CRLHandler: nv.NewCRLHandler(), }, nil } diff --git a/pkg/keymanagementprovider/refresh/kubeRefresh_test.go b/pkg/keymanagementprovider/refresh/kubeRefresh_test.go index 9875098b8..0e930f931 100644 --- a/pkg/keymanagementprovider/refresh/kubeRefresh_test.go +++ b/pkg/keymanagementprovider/refresh/kubeRefresh_test.go @@ -21,14 +21,19 @@ import ( "crypto" "crypto/x509" "errors" + "net/http" "reflect" "testing" "time" + "github.com/notaryproject/notation-core-go/revocation" + corecrl "github.com/notaryproject/notation-core-go/revocation/crl" + re "github.com/ratify-project/ratify/errors" "github.com/ratify-project/ratify/pkg/keymanagementprovider" "github.com/ratify-project/ratify/pkg/keymanagementprovider/config" _ "github.com/ratify-project/ratify/pkg/keymanagementprovider/inline" mock "github.com/ratify-project/ratify/pkg/keymanagementprovider/mocks" + nv "github.com/ratify-project/ratify/pkg/verifier/notation" ctrl "sigs.k8s.io/controller-runtime" ) @@ -41,6 +46,7 @@ func TestKubeRefresher_Refresh(t *testing.T) { GetCertsFunc func(_ context.Context) (map[keymanagementprovider.KMPMapKey][]*x509.Certificate, keymanagementprovider.KeyManagementProviderStatus, error) GetKeysFunc func(_ context.Context) (map[keymanagementprovider.KMPMapKey]crypto.PublicKey, keymanagementprovider.KeyManagementProviderStatus, error) IsRefreshableFunc func() bool + NewCRLHandler nv.RevocationFactory expectedResult ctrl.Result expectedError bool }{ @@ -49,6 +55,7 @@ func TestKubeRefresher_Refresh(t *testing.T) { providerRawParameters: []byte(`{"contentType": "certificate", "value": "-----BEGIN CERTIFICATE-----\nMIID2jCCAsKgAwIBAgIQXy2VqtlhSkiZKAGhsnkjbDANBgkqhkiG9w0BAQsFADBvMRswGQYDVQQD\nExJyYXRpZnkuZXhhbXBsZS5jb20xDzANBgNVBAsTBk15IE9yZzETMBEGA1UEChMKTXkgQ29tcGFu\neTEQMA4GA1UEBxMHUmVkbW9uZDELMAkGA1UECBMCV0ExCzAJBgNVBAYTAlVTMB4XDTIzMDIwMTIy\nNDUwMFoXDTI0MDIwMTIyNTUwMFowbzEbMBkGA1UEAxMScmF0aWZ5LmV4YW1wbGUuY29tMQ8wDQYD\nVQQLEwZNeSBPcmcxEzARBgNVBAoTCk15IENvbXBhbnkxEDAOBgNVBAcTB1JlZG1vbmQxCzAJBgNV\nBAgTAldBMQswCQYDVQQGEwJVUzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL10bM81\npPAyuraORABsOGS8M76Bi7Guwa3JlM1g2D8CuzSfSTaaT6apy9GsccxUvXd5cmiP1ffna5z+EFmc\nizFQh2aq9kWKWXDvKFXzpQuhyqD1HeVlRlF+V0AfZPvGt3VwUUjNycoUU44ctCWmcUQP/KShZev3\n6SOsJ9q7KLjxxQLsUc4mg55eZUThu8mGB8jugtjsnLUYvIWfHhyjVpGrGVrdkDMoMn+u33scOmrt\nsBljvq9WVo4T/VrTDuiOYlAJFMUae2Ptvo0go8XTN3OjLblKeiK4C+jMn9Dk33oGIT9pmX0vrDJV\nX56w/2SejC1AxCPchHaMuhlwMpftBGkCAwEAAaNyMHAwDgYDVR0PAQH/BAQDAgeAMAkGA1UdEwQC\nMAAwEwYDVR0lBAwwCgYIKwYBBQUHAwMwHwYDVR0jBBgwFoAU0eaKkZj+MS9jCp9Dg1zdv3v/aKww\nHQYDVR0OBBYEFNHmipGY/jEvYwqfQ4Nc3b97/2isMA0GCSqGSIb3DQEBCwUAA4IBAQBNDcmSBizF\nmpJlD8EgNcUCy5tz7W3+AAhEbA3vsHP4D/UyV3UgcESx+L+Nye5uDYtTVm3lQejs3erN2BjW+ds+\nXFnpU/pVimd0aYv6mJfOieRILBF4XFomjhrJOLI55oVwLN/AgX6kuC3CJY2NMyJKlTao9oZgpHhs\nLlxB/r0n9JnUoN0Gq93oc1+OLFjPI7gNuPXYOP1N46oKgEmAEmNkP1etFrEjFRgsdIFHksrmlOlD\nIed9RcQ087VLjmuymLgqMTFX34Q3j7XgN2ENwBSnkHotE9CcuGRW+NuiOeJalL8DBmFXXWwHTKLQ\nPp5g6m1yZXylLJaFLKz7tdMmO355\n-----END CERTIFICATE-----\n"}`), providerType: "inline", IsRefreshableFunc: func() bool { return false }, + NewCRLHandler: nv.NewCRLHandler(), expectedResult: ctrl.Result{}, expectedError: false, }, @@ -57,6 +64,7 @@ func TestKubeRefresher_Refresh(t *testing.T) { providerRawParameters: []byte(`{"vaultURI": "https://yourkeyvault.vault.azure.net/", "certificates": [{"name": "cert1", "version": "1"}], "tenantID": "yourtenantID", "clientID": "yourclientID"}`), providerType: "test-kmp", providerRefreshInterval: "", + NewCRLHandler: nv.NewCRLHandler(), IsRefreshableFunc: func() bool { return true }, expectedResult: ctrl.Result{}, expectedError: false, @@ -66,6 +74,7 @@ func TestKubeRefresher_Refresh(t *testing.T) { providerRawParameters: []byte(`{"vaultURI": "https://yourkeyvault.vault.azure.net/", "certificates": [{"name": "cert1", "version": "1"}], "tenantID": "yourtenantID", "clientID": "yourclientID"}`), providerType: "test-kmp", providerRefreshInterval: "1m", + NewCRLHandler: nv.NewCRLHandler(), IsRefreshableFunc: func() bool { return true }, expectedResult: ctrl.Result{RequeueAfter: time.Minute}, expectedError: false, @@ -75,6 +84,7 @@ func TestKubeRefresher_Refresh(t *testing.T) { providerRawParameters: []byte(`{"vaultURI": "https://yourkeyvault.vault.azure.net/", "certificates": [{"name": "cert1", "version": "1"}], "tenantID": "yourtenantID", "clientID": "yourclientID"}`), providerType: "test-kmp", providerRefreshInterval: "1mm", + NewCRLHandler: nv.NewCRLHandler(), IsRefreshableFunc: func() bool { return true }, expectedResult: ctrl.Result{}, expectedError: true, @@ -88,6 +98,7 @@ func TestKubeRefresher_Refresh(t *testing.T) { providerRawParameters: []byte(`{"vaultURI": "https://yourkeyvault.vault.azure.net/", "certificates": [{"name": "cert1", "version": "1"}], "tenantID": "yourtenantID", "clientID": "yourclientID"}`), providerType: "test-kmp-error", IsRefreshableFunc: func() bool { return true }, + NewCRLHandler: nv.NewCRLHandler(), expectedError: true, }, { @@ -99,14 +110,29 @@ func TestKubeRefresher_Refresh(t *testing.T) { providerRawParameters: []byte(`{"vaultURI": "https://yourkeyvault.vault.azure.net/", "certificates": [{"name": "cert1", "version": "1"}], "tenantID": "yourtenantID", "clientID": "yourclientID"}`), providerType: "test-kmp-error", IsRefreshableFunc: func() bool { return true }, + NewCRLHandler: nv.NewCRLHandler(), expectedError: true, }, + { + name: "Error Caching with CRL Fetcher (non-blocking)", + GetCertsFunc: func(_ context.Context) (map[keymanagementprovider.KMPMapKey][]*x509.Certificate, keymanagementprovider.KeyManagementProviderStatus, error) { + return map[keymanagementprovider.KMPMapKey][]*x509.Certificate{ + {Name: "sample"}: {&x509.Certificate{}}, + }, keymanagementprovider.KeyManagementProviderStatus{}, nil + }, + providerRawParameters: []byte(`{"vaultURI": "https://yourkeyvault.vault.azure.net/", "certificates": [{"name": "cert1", "version": "1"}], "tenantID": "yourtenantID", "clientID": "yourclientID"}`), + providerType: "test-kmp", + providerRefreshInterval: "1m", + IsRefreshableFunc: func() bool { return true }, + NewCRLHandler: &MockCRLHandler{CacheEnabled: true, httpClient: &http.Client{}}, + expectedResult: ctrl.Result{RequeueAfter: time.Minute}, + expectedError: false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var factory mock.TestKeyManagementProviderFactory - if tt.GetCertsFunc != nil { factory = mock.TestKeyManagementProviderFactory{ GetCertsFunc: tt.GetCertsFunc, @@ -130,6 +156,7 @@ func TestKubeRefresher_Refresh(t *testing.T) { ProviderType: tt.providerType, ProviderRefreshInterval: tt.providerRefreshInterval, Resource: "kmpname", + CRLHandler: tt.NewCRLHandler, } err := kr.Refresh(context.Background()) @@ -144,9 +171,24 @@ func TestKubeRefresher_Refresh(t *testing.T) { } } +type MockCRLHandler struct { + CacheEnabled bool + Fetcher corecrl.Fetcher + httpClient *http.Client +} + +func (h *MockCRLHandler) NewFetcher() (corecrl.Fetcher, error) { + return nil, re.ErrorCodeConfigInvalid.WithDetail("failed to create CRL fetcher") +} + +func (h *MockCRLHandler) NewValidator(_ revocation.Options) (revocation.Validator, error) { + return nil, nil +} + func TestKubeRefresher_GetResult(t *testing.T) { kr := &KubeRefresher{ - Result: ctrl.Result{RequeueAfter: time.Minute}, + Result: ctrl.Result{RequeueAfter: time.Minute}, + CRLHandler: nv.NewCRLHandler(), } result := kr.GetResult() @@ -162,6 +204,7 @@ func TestKubeRefresher_GetStatus(t *testing.T) { "attribute1": "value1", "attribute2": "value2", }, + CRLHandler: nv.NewCRLHandler(), } status := kr.GetStatus() @@ -210,7 +253,7 @@ func TestKubeRefresher_Create(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - kr := &KubeRefresher{} + kr := &KubeRefresher{CRLHandler: nv.NewCRLHandler()} refresher, err := kr.Create(tt.config) if err != nil { t.Fatalf("Expected no error, but got %v", err) diff --git a/pkg/verifier/notation/notation.go b/pkg/verifier/notation/notation.go index 52fdcbda6..de54857f4 100644 --- a/pkg/verifier/notation/notation.go +++ b/pkg/verifier/notation/notation.go @@ -95,7 +95,8 @@ func init() { } func (f *notationPluginVerifierFactory) Create(_ string, verifierConfig config.VerifierConfig, pluginDirectory string, namespace string) (verifier.ReferenceVerifier, error) { - logger.GetLogger(context.Background(), logOpt).Debugf("creating Notation verifier with config %v, namespace '%v'", verifierConfig, namespace) + ctx := context.Background() + logger.GetLogger(ctx, logOpt).Debugf("creating Notation verifier with config %v, namespace '%v'", verifierConfig, namespace) verifierName := fmt.Sprintf("%s", verifierConfig[types.Name]) verifierTypeStr := "" if _, ok := verifierConfig[types.Type]; ok { @@ -105,7 +106,7 @@ func (f *notationPluginVerifierFactory) Create(_ string, verifierConfig config.V if err != nil { return nil, re.ErrorCodePluginInitFailure.WithDetail("Failed to create the Notation Verifier").WithError(err) } - verifyService, err := getVerifierService(conf, pluginDirectory, NewRevocationFactoryImpl()) + verifyService, err := getVerifierService(ctx, conf, pluginDirectory, NewCRLHandler()) if err != nil { return nil, re.ErrorCodePluginInitFailure.WithDetail("Failed to create the Notation Verifier").WithError(err) } @@ -177,7 +178,7 @@ func (v *notationPluginVerifier) Verify(ctx context.Context, return verifier.NewVerifierResult("", v.name, v.verifierType, "Notation signature verification success", true, nil, extensions), nil } -func getVerifierService(conf *NotationPluginVerifierConfig, pluginDirectory string, revocationFactory RevocationFactory) (notation.Verifier, error) { +func getVerifierService(ctx context.Context, conf *NotationPluginVerifierConfig, pluginDirectory string, revocationFactory RevocationFactory) (notation.Verifier, error) { store, err := newTrustStore(conf.VerificationCerts, conf.VerificationCertStores) if err != nil { return nil, err @@ -190,7 +191,7 @@ func getVerifierService(conf *NotationPluginVerifierConfig, pluginDirectory stri // Related File: https://github.com/notaryproject/notation/commits/main/cmd/notation/verify.go5 crlFetcher, err := revocationFactory.NewFetcher() if err != nil { - return nil, err + logger.GetLogger(ctx, logOpt).Warnf("Unable to create CRL fetcher for notation verifier %s with error: %s", conf.Name, err) } revocationCodeSigningValidator, err := revocationFactory.NewValidator(revocation.Options{ CRLFetcher: crlFetcher, diff --git a/pkg/verifier/notation/notation_test.go b/pkg/verifier/notation/notation_test.go index bf2fa4abb..c0ae0d226 100644 --- a/pkg/verifier/notation/notation_test.go +++ b/pkg/verifier/notation/notation_test.go @@ -625,7 +625,7 @@ func TestGetVerifierService(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - _, err := getVerifierService(tt.conf, tt.pluginDir, tt.RevocationFactory) + _, err := getVerifierService(context.Background(), tt.conf, tt.pluginDir, tt.RevocationFactory) if (err != nil) != tt.expectErr { t.Errorf("error = %v, expectErr = %v", err, tt.expectErr) } diff --git a/pkg/verifier/notation/notationrevocationfactory.go b/pkg/verifier/notation/notationrevocationfactory.go index 47cc35606..b8687f5ea 100644 --- a/pkg/verifier/notation/notationrevocationfactory.go +++ b/pkg/verifier/notation/notationrevocationfactory.go @@ -15,49 +15,63 @@ package notation import ( "net/http" + "sync" "github.com/notaryproject/notation-core-go/revocation" corecrl "github.com/notaryproject/notation-core-go/revocation/crl" "github.com/notaryproject/notation-go/dir" - "github.com/notaryproject/notation-go/verifier/crl" + re "github.com/ratify-project/ratify/errors" ) -type RevocationFactoryImpl struct { - cacheRoot string - httpClient *http.Client +type CRLHandler struct { + CacheEnabled bool + Fetcher corecrl.Fetcher + httpClient *http.Client } -// NewRevocationFactoryImpl returns a new NewRevocationFactoryImpl instance -func NewRevocationFactoryImpl() RevocationFactory { - return &RevocationFactoryImpl{ - cacheRoot: dir.PathCRLCache, - httpClient: &http.Client{}, - } +var fetcherOnce sync.Once + +// NewCRLHandler returns a new NewCRLHandler instance. Enable cache by default. +func NewCRLHandler() RevocationFactory { + return &CRLHandler{CacheEnabled: true, httpClient: &http.Client{}} } -// NewFetcher returns a new fetcher instance -func (f *RevocationFactoryImpl) NewFetcher() (corecrl.Fetcher, error) { - crlFetcher, err := corecrl.NewHTTPFetcher(f.httpClient) +// NewFetcher creates a new instance of a Fetcher if it doesn't already exist. +// If a Fetcher instance is already present, it returns the existing instance. +// The method also configures the cache for the Fetcher. +// Returns an instance of corecrl.Fetcher or an error if the Fetcher creation fails. +func (h *CRLHandler) NewFetcher() (corecrl.Fetcher, error) { + var err error + fetcherOnce.Do(func() { + h.Fetcher, err = CreateCRLFetcher(h.httpClient, dir.PathCRLCache) + if err == nil { + h.configureCache() + } + }) if err != nil { return nil, err } - crlFetcher.Cache, err = newFileCache(f.cacheRoot) - if err != nil { - return nil, err + // Check if the fetcher is nil, return an error if it is. + // one possible edge case is that an error happened in the first call, + // the following calls will not get the error since the sync.Once block will be skipped. + if h.Fetcher == nil { + return nil, re.ErrorCodeConfigInvalid.WithDetail("failed to create CRL fetcher") } - return crlFetcher, nil + return h.Fetcher, nil } // NewValidator returns a new validator instance -func (f *RevocationFactoryImpl) NewValidator(opts revocation.Options) (revocation.Validator, error) { +func (h *CRLHandler) NewValidator(opts revocation.Options) (revocation.Validator, error) { return revocation.NewWithOptions(opts) } -// newFileCache returns a new file cache instance -func newFileCache(root string) (*crl.FileCache, error) { - cacheRoot, err := dir.CacheFS().SysPath(root) - if err != nil { - return nil, err +// configureCache disables the cache for the HTTPFetcher if caching is not enabled. +// If the EnableCache field is set to false, this method sets the Cache field of the +// HTTPFetcher to nil, effectively disabling caching for HTTP fetch operations. +func (h *CRLHandler) configureCache() { + if !h.CacheEnabled { + if httpFetcher, ok := h.Fetcher.(*corecrl.HTTPFetcher); ok { + httpFetcher.Cache = nil + } } - return crl.NewFileCache(cacheRoot) } diff --git a/pkg/verifier/notation/notationrevocationfactory_test.go b/pkg/verifier/notation/notationrevocationfactory_test.go index b5355f83c..d30e619b3 100644 --- a/pkg/verifier/notation/notationrevocationfactory_test.go +++ b/pkg/verifier/notation/notationrevocationfactory_test.go @@ -14,16 +14,20 @@ package notation import ( + "context" "net/http" "runtime" "testing" "github.com/notaryproject/notation-core-go/revocation" + corecrl "github.com/notaryproject/notation-core-go/revocation/crl" + "github.com/notaryproject/notation-go/dir" + "github.com/notaryproject/notation-go/verifier/crl" "github.com/stretchr/testify/assert" ) func TestNewRevocationFactoryImpl(t *testing.T) { - factory := NewRevocationFactoryImpl() + factory := NewCRLHandler() assert.NotNil(t, factory) } @@ -41,8 +45,8 @@ func TestNewFetcher(t *testing.T) { wantErr: false, }, { - name: "invalid fetcher with nil httpClient", - cacheRoot: "/valid/path", + name: "invalid fetcher", + cacheRoot: "", httpClient: nil, wantErr: true, }, @@ -50,11 +54,7 @@ func TestNewFetcher(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - factory := &RevocationFactoryImpl{ - cacheRoot: tt.cacheRoot, - httpClient: tt.httpClient, - } - + factory := &CRLHandler{httpClient: tt.httpClient} fetcher, err := factory.NewFetcher() if tt.wantErr { assert.Error(t, err) @@ -65,7 +65,7 @@ func TestNewFetcher(t *testing.T) { } func TestNewValidator(t *testing.T) { - factory := &RevocationFactoryImpl{} + factory := NewCRLHandler() opts := revocation.Options{} validator, err := factory.NewValidator(opts) @@ -101,3 +101,55 @@ func TestNewFileCache(t *testing.T) { }) } } +func TestConfigureCache(t *testing.T) { + testCache, _ := crl.NewFileCache(dir.PathCRLCache) + tests := []struct { + name string + cacheEnabled bool + fetcher corecrl.Fetcher + expectCache bool + }{ + { + name: "cache enabled", + cacheEnabled: true, + fetcher: &corecrl.HTTPFetcher{Cache: testCache}, + expectCache: true, + }, + { + name: "cache disabled", + cacheEnabled: false, + fetcher: &corecrl.HTTPFetcher{Cache: testCache}, + expectCache: false, + }, + { + name: "non-HTTP fetcher", + cacheEnabled: false, + fetcher: &mockFetcher{}, + expectCache: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + handler := &CRLHandler{ + CacheEnabled: tt.cacheEnabled, + Fetcher: tt.fetcher, + } + handler.configureCache() + + if httpFetcher, ok := handler.Fetcher.(*corecrl.HTTPFetcher); ok { + if tt.expectCache { + assert.NotNil(t, httpFetcher.Cache) + } else { + assert.Nil(t, httpFetcher.Cache) + } + } + }) + } +} + +type mockFetcher struct{} + +func (m *mockFetcher) Fetch(_ context.Context, _ string) (*corecrl.Bundle, error) { + return nil, nil +} diff --git a/pkg/verifier/notation/revocationfactory.go b/pkg/verifier/notation/revocationfactory.go index 7860ec2a7..d0c576fe0 100644 --- a/pkg/verifier/notation/revocationfactory.go +++ b/pkg/verifier/notation/revocationfactory.go @@ -14,10 +14,21 @@ package notation import ( + "context" + "crypto/x509" + "net/http" + "sync" + "github.com/notaryproject/notation-core-go/revocation" corecrl "github.com/notaryproject/notation-core-go/revocation/crl" + "github.com/notaryproject/notation-go/dir" + "github.com/notaryproject/notation-go/verifier/crl" + "github.com/ratify-project/ratify/internal/logger" ) +// RevocationFactory is an interface that defines methods for creating instances +// related to revocation. It provides methods to create a new fetcher and a new +// validator. type RevocationFactory interface { // NewFetcher returns a new fetcher instance NewFetcher() (corecrl.Fetcher, error) @@ -25,3 +36,62 @@ type RevocationFactory interface { // NewValidator returns a new validator instance NewValidator(revocation.Options) (revocation.Validator, error) } + +// CreateCRLFetcher returns a new fetcher instance +func CreateCRLFetcher(httpClient *http.Client, cacheRoot string) (corecrl.Fetcher, error) { + crlFetcher, err := corecrl.NewHTTPFetcher(httpClient) + if err != nil { + return nil, err + } + crlFetcher.Cache, err = newFileCache(cacheRoot) + if err != nil { + return nil, err + } + return crlFetcher, nil +} + +// SupportCRL checks if the certificate supports CRL +func SupportCRL(cert *x509.Certificate) bool { + return cert != nil && len(cert.CRLDistributionPoints) > 0 +} + +// cacheCRL caches the Certificate Revocation Lists (CRLs) for the given certificates using the provided CRL fetcher. +// It logs a warning if fetching the CRL fails but does not return an error to ensure the process is not blocked. +func CacheCRL(ctx context.Context, certs []*x509.Certificate, fetcher corecrl.Fetcher) { + if fetcher == nil { + logger.GetLogger(ctx, logOpt).Warn("CRL fetcher is nil") + return + } + var wg sync.WaitGroup + for _, cert := range certs { + if !SupportCRL(cert) { + continue + } + cacheCertificateCRL(ctx, cert.CRLDistributionPoints, fetcher, &wg) + } + wg.Wait() +} + +func cacheCertificateCRL(ctx context.Context, crlURLs []string, crlFetcher corecrl.Fetcher, wg *sync.WaitGroup) { + for _, crlURL := range crlURLs { + crlURL := crlURL // capture loop variable + wg.Add(1) + go fetchCRL(ctx, crlURL, crlFetcher, wg) + } +} + +func fetchCRL(ctx context.Context, url string, crlFetcher corecrl.Fetcher, wg *sync.WaitGroup) { + defer wg.Done() + if _, err := crlFetcher.Fetch(ctx, url); err != nil { + logger.GetLogger(ctx, logOpt).Errorf("failed to download CRL from %s : %v", url, err) + } +} + +// newFileCache returns a new file cache instance +func newFileCache(root string) (*crl.FileCache, error) { + cacheRoot, err := dir.CacheFS().SysPath(root) + if err != nil { + return nil, err + } + return crl.NewFileCache(cacheRoot) +} diff --git a/pkg/verifier/notation/revocationfactory_test.go b/pkg/verifier/notation/revocationfactory_test.go new file mode 100644 index 000000000..8b295031d --- /dev/null +++ b/pkg/verifier/notation/revocationfactory_test.go @@ -0,0 +1,143 @@ +// Copyright The Ratify Authors. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package notation + +import ( + "context" + "crypto/x509" + "fmt" + "net/http" + "testing" + + corecrl "github.com/notaryproject/notation-core-go/revocation/crl" + "github.com/stretchr/testify/assert" +) + +func TestCRLNewFetcher(t *testing.T) { + httpClient := &http.Client{} + cacheRoot := "/tmp/cache" + + t.Run("successful fetcher creation", func(t *testing.T) { + fetcher, err := CreateCRLFetcher(httpClient, cacheRoot) + assert.NoError(t, err) + assert.NotNil(t, fetcher) + }) + + t.Run("error in creating HTTP fetcher", func(t *testing.T) { + // Simulate error by passing nil httpClient + fetcher, err := CreateCRLFetcher(nil, cacheRoot) + assert.Error(t, err) + assert.Nil(t, fetcher) + }) +} +func TestSupportCRL(t *testing.T) { + t.Run("certificate with CRL distribution points", func(t *testing.T) { + cert := &x509.Certificate{ + CRLDistributionPoints: []string{"http://example.com/crl"}, + } + assert.True(t, SupportCRL(cert)) + }) + + t.Run("certificate without CRL distribution points", func(t *testing.T) { + cert := &x509.Certificate{} + assert.False(t, SupportCRL(cert)) + }) + + t.Run("nil certificate", func(t *testing.T) { + assert.False(t, SupportCRL(nil)) + }) +} +func TestCacheCRL(t *testing.T) { + ctx := context.Background() + httpClient := &http.Client{} + cacheRoot := "/tmp/cache" + fetcher, _ := CreateCRLFetcher(httpClient, cacheRoot) + + t.Run("nil fetcher", func(t *testing.T) { + certs := []*x509.Certificate{ + { + CRLDistributionPoints: []string{"http://example.com/crl"}, + }, + } + CacheCRL(ctx, certs, nil) + // Check logs if necessary + t.Log("CRL fetcher is nil") + }) + + t.Run("certificate without CRL distribution points", func(t *testing.T) { + certs := []*x509.Certificate{ + {}, + } + CacheCRL(ctx, certs, fetcher) + // Check logs if necessary + t.Log("Certificate does not support CRL") + }) + + t.Run("certificates with CRL distribution points", func(t *testing.T) { + certs := []*x509.Certificate{ + { + CRLDistributionPoints: []string{"http://example.com/crl1"}, + }, + { + CRLDistributionPoints: []string{"http://example.com/crl2"}, + }, + } + CacheCRL(ctx, certs, fetcher) + // Check logs if necessary + t.Log("Completed fetching CRLs") + }) +} +func TestIntermittentFailCacheCRL(t *testing.T) { + ctx := context.Background() + t.Run("fetch CRL fails", func(t *testing.T) { + // Mock fetcher to simulate failure + mockFetcher := &MockFetcher{ + flag: true, + FetchFunc: func(_ context.Context, _ string) (*corecrl.Bundle, error) { + return &corecrl.Bundle{}, nil + }, + } + certs := []*x509.Certificate{ + { + CRLDistributionPoints: []string{"http://example.com/crl1"}, + }, + { + CRLDistributionPoints: []string{"http://example.com/crl2"}, + }, + { + CRLDistributionPoints: []string{"http://example.com/crl3"}, + }, + { + CRLDistributionPoints: []string{"http://example.com/crl4"}, + }, + } + CacheCRL(ctx, certs, mockFetcher) + // Check logs if necessary + t.Log("Completed fetching CRLs with intermittent failures") + }) +} + +// MockFetcher is a mock implementation of corecrl.Fetcher for testing purposes +type MockFetcher struct { + flag bool + FetchFunc func(ctx context.Context, url string) (*corecrl.Bundle, error) +} + +func (m *MockFetcher) Fetch(ctx context.Context, url string) (*corecrl.Bundle, error) { + m.flag = !m.flag + if m.flag { + return nil, fmt.Errorf("failed to fetch CRL from %s", url) + } + return m.FetchFunc(ctx, url) +}