Repository: ratify-project/ratify (go)

Issue:
feat: CRL fetcher
### What would you like to be added?

Implement CRL fetcher based on fetcher interface provided by notation-core-go.


### Anything else you would like to add?

_No response_

### Are you willing to submit PRs to contribute to this feature?

- [X] Yes, I am willing to implement it.

Pull request:
feat: add support for crl basic functionality with built-in cache
# Description

Enables CRL revocation with file cache during verification

## What this PR does / why we need it:
<!--Please include a summary of the changes and relevant context. -->

The implementation is from notation cli covering CRL support CRL file-based cache support.
Removed HTTP timeout.
Related PR: https://github.com/notaryproject/notation/pull/1043
Related File: https://github.com/notaryproject/notation/commits/main/cmd/notation/verify.go



## Which issue(s) this PR fixes *(optional, using `fixes #<issue number>(, fixes #<issue_number>, ...)` format, will close the issue(s) when the PR gets merged)*:
Fixes #1889

## Type of change

Please delete options that are not relevant.

- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] Helm Chart Change (any edit/addition/update that is necessary for changes merged to the `main` branch)
- [ ] This change requires a documentation update

# How Has This Been Tested?

Manual test scenario: verify a signature with revocation check enabled.
![image](https://github.com/user-attachments/assets/be63dc35-bbd8-490d-97f2-97fd9e31fa42)

# Checklist:

- [x] Does the affected code have corresponding tests?
- [ ] Are the changes documented, not just with inline documentation, but also with conceptual documentation such as an overview of a new feature, or task-based documentation like a tutorial? Consider if this change should be announced on your project blog.
- [ ] Does this introduce breaking changes that would require an announcement or bumping the major version?
- [x] Do all new files have appropriate license header?

# Post Merge Requirements
- [ ] MAINTAINERS: manually trigger the "Publish Package" workflow after merging any PR that indicates `Helm Chart Change`


Patch:
diff --git a/go.mod b/go.mod
index f252a67..32985ce 100644
--- a/go.mod
+++ b/go.mod
@@ -29,8 +29,8 @@ 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.1.0
-	github.com/notaryproject/notation-go v1.2.1
+	github.com/notaryproject/notation-core-go v1.2.0-rc.1
+	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
 	github.com/open-policy-agent/frameworks/constraint v0.0.0-20230411224310-3f237e2710fa
@@ -238,7 +238,7 @@ require (
 	go.uber.org/zap v1.27.0 // indirect
 	golang.org/x/crypto v0.28.0
 	golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 // indirect
-	golang.org/x/mod v0.20.0 // indirect
+	golang.org/x/mod v0.21.0 // indirect
 	golang.org/x/net v0.29.0 // indirect
 	golang.org/x/oauth2 v0.23.0 // indirect
 	golang.org/x/sys v0.26.0 // indirect
diff --git a/go.sum b/go.sum
index 47b4ca6..72451b6 100644
--- a/go.sum
+++ b/go.sum
@@ -519,10 +519,10 @@ 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.1.0 h1:xCybcONOKcCyPNihJUSa+jRNsyQFNkrk0eJVVs1kWeg=
-github.com/notaryproject/notation-core-go v1.1.0/go.mod h1:+6AOh41JPrnVLbW/19SJqdhVHwKgIINBO/np0e7nXJA=
-github.com/notaryproject/notation-go v1.2.1 h1:fbCMBcvg1xttrisd5CyM60QDectGYYF701Us0M3cKN8=
-github.com/notaryproject/notation-go v1.2.1/go.mod h1:re9V+TfuNRaUq5e3NuNcCJN53++sL2KbnJrjGyOUpgE=
+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-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=
@@ -796,8 +796,8 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
 golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
-golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0=
-golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
+golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
diff --git a/pkg/verifier/notation/notation.go b/pkg/verifier/notation/notation.go
index e825db1..52fdcbd 100644
--- a/pkg/verifier/notation/notation.go
+++ b/pkg/verifier/notation/notation.go
@@ -36,6 +36,8 @@ import (
 	"github.com/ratify-project/ratify/pkg/verifier/factory"
 	"github.com/ratify-project/ratify/pkg/verifier/types"
 
+	"github.com/notaryproject/notation-core-go/revocation"
+	"github.com/notaryproject/notation-core-go/revocation/purpose"
 	_ "github.com/notaryproject/notation-core-go/signature/cose" // register COSE signature
 	_ "github.com/notaryproject/notation-core-go/signature/jws"  // register JWS signature
 	"github.com/notaryproject/notation-go"
@@ -103,8 +105,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)
+	verifyService, err := getVerifierService(conf, pluginDirectory, NewRevocationFactoryImpl())
 	if err != nil {
 		return nil, re.ErrorCodePluginInitFailure.WithDetail("Failed to create the Notation Verifier").WithError(err)
 	}
@@ -176,12 +177,40 @@ 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) (notation.Verifier, error) {
+func getVerifierService(conf *NotationPluginVerifierConfig, pluginDirectory string, revocationFactory RevocationFactory) (notation.Verifier, error) {
 	store, err := newTrustStore(conf.VerificationCerts, conf.VerificationCertStores)
 	if err != nil {
 		return nil, err
 	}
-	verifier, err := notationVerifier.New(&conf.TrustPolicyDoc, store, NewRatifyPluginManager(pluginDirectory))
+
+	// revocation check using corecrl from notation-core-go and crl from notation-go
+	// This is the implementation for revocation check from notation cli to support crl and cache configurations
+	// removed timeout
+	// Related PR: notaryproject/notation#1043
+	// Related File: https://github.com/notaryproject/notation/commits/main/cmd/notation/verify.go5
+	crlFetcher, err := revocationFactory.NewFetcher()
+	if err != nil {
+		return nil, err
+	}
+	revocationCodeSigningValidator, err := revocationFactory.NewValidator(revocation.Options{
+		CRLFetcher:       crlFetcher,
+		CertChainPurpose: purpose.CodeSigning,
+	})
+	if err != nil {
+		return nil, err
+	}
+	revocationTimestampingValidator, err := revocationFactory.NewValidator(revocation.Options{
+		CRLFetcher:       crlFetcher,
+		CertChainPurpose: purpose.Timestamping,
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	verifier, err := notationVerifier.NewWithOptions(&conf.TrustPolicyDoc, store, NewRatifyPluginManager(pluginDirectory), notationVerifier.VerifierOptions{
+		RevocationCodeSigningValidator:  revocationCodeSigningValidator,
+		RevocationTimestampingValidator: revocationTimestampingValidator,
+	})
 	if err != nil {
 		return nil, re.ErrorCodePluginInitFailure.WithDetail("Failed to create the Notation Verifier").WithError(err)
 	}
diff --git a/pkg/verifier/notation/notationrevocationfactory.go b/pkg/verifier/notation/notationrevocationfactory.go
new file mode 100644
index 0000000..47cc356
--- /dev/null
+++ b/pkg/verifier/notation/notationrevocationfactory.go
@@ -0,0 +1,63 @@
+// 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 (
+	"net/http"
+
+	"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"
+)
+
+type RevocationFactoryImpl struct {
+	cacheRoot  string
+	httpClient *http.Client
+}
+
+// NewRevocationFactoryImpl returns a new NewRevocationFactoryImpl instance
+func NewRevocationFactoryImpl() RevocationFactory {
+	return &RevocationFactoryImpl{
+		cacheRoot:  dir.PathCRLCache,
+		httpClient: &http.Client{},
+	}
+}
+
+// NewFetcher returns a new fetcher instance
+func (f *RevocationFactoryImpl) NewFetcher() (corecrl.Fetcher, error) {
+	crlFetcher, err := corecrl.NewHTTPFetcher(f.httpClient)
+	if err != nil {
+		return nil, err
+	}
+	crlFetcher.Cache, err = newFileCache(f.cacheRoot)
+	if err != nil {
+		return nil, err
+	}
+	return crlFetcher, nil
+}
+
+// NewValidator returns a new validator instance
+func (f *RevocationFactoryImpl) 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
+	}
+	return crl.NewFileCache(cacheRoot)
+}
diff --git a/pkg/verifier/notation/revocationfactory.go b/pkg/verifier/notation/revocationfactory.go
new file mode 100644
index 0000000..7860ec2
--- /dev/null
+++ b/pkg/verifier/notation/revocationfactory.go
@@ -0,0 +1,27 @@
+// 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 (
+	"github.com/notaryproject/notation-core-go/revocation"
+	corecrl "github.com/notaryproject/notation-core-go/revocation/crl"
+)
+
+type RevocationFactory interface {
+	// NewFetcher returns a new fetcher instance
+	NewFetcher() (corecrl.Fetcher, error)
+
+	// NewValidator returns a new validator instance
+	NewValidator(revocation.Options) (revocation.Validator, error)
+}


Test patch:
diff --git a/pkg/verifier/notation/notation_test.go b/pkg/verifier/notation/notation_test.go
index dc34a1f..bf2fa4a 100644
--- a/pkg/verifier/notation/notation_test.go
+++ b/pkg/verifier/notation/notation_test.go
@@ -18,10 +18,14 @@ import (
 	"crypto/x509"
 	"crypto/x509/pkix"
 	"fmt"
+	"net/http"
 	paths "path/filepath"
 	"reflect"
 	"testing"
 
+	"github.com/notaryproject/notation-core-go/revocation"
+	corecrl "github.com/notaryproject/notation-core-go/revocation/crl"
+	"github.com/notaryproject/notation-core-go/revocation/purpose"
 	sig "github.com/notaryproject/notation-core-go/signature"
 	"github.com/notaryproject/notation-go"
 	"github.com/opencontainers/go-digest"
@@ -558,3 +562,102 @@ func TestNormalizeLegacyCertStore(t *testing.T) {
 		})
 	}
 }
+func TestGetVerifierService(t *testing.T) {
+	tests := []struct {
+		name              string
+		conf              *NotationPluginVerifierConfig
+		pluginDir         string
+		RevocationFactory RevocationFactory
+		expectErr         bool
+		errContent        error
+	}{
+		{
+			name: "failed to create CRL fetcher",
+			conf: &NotationPluginVerifierConfig{
+				VerificationCerts: []string{defaultCertDir},
+			},
+			pluginDir:         "",
+			RevocationFactory: mockRevocationFactory{httpClient: &http.Client{}, failFetcher: true},
+			expectErr:         true,
+			errContent:        nil,
+		},
+		{
+			name: "failed to create file cache",
+			conf: &NotationPluginVerifierConfig{
+				VerificationCerts: []string{defaultCertDir},
+			},
+			pluginDir:         "",
+			RevocationFactory: mockRevocationFactory{httpClient: &http.Client{}, failFileCache: true},
+			expectErr:         true,
+			errContent:        nil,
+		},
+		{
+			name: "failed to create code signing validator",
+			conf: &NotationPluginVerifierConfig{
+				VerificationCerts: []string{defaultCertDir},
+			},
+			pluginDir:         "",
+			RevocationFactory: mockRevocationFactory{httpClient: &http.Client{}, failCodeSigningValidator: true},
+			expectErr:         true,
+			errContent:        fmt.Errorf("failed to create code signing validator"),
+		},
+		{
+			name: "failed to create timestamping validator",
+			conf: &NotationPluginVerifierConfig{
+				VerificationCerts: []string{defaultCertDir},
+			},
+			pluginDir:         "",
+			RevocationFactory: mockRevocationFactory{httpClient: &http.Client{}, failTimestampingValidator: true},
+			expectErr:         true,
+			errContent:        fmt.Errorf("failed to create timestamping validator"),
+		},
+		{
+			name: "failed to create verifier",
+			conf: &NotationPluginVerifierConfig{
+				VerificationCerts: []string{defaultCertDir},
+			},
+			pluginDir:         "",
+			RevocationFactory: mockRevocationFactory{httpClient: &http.Client{}, failVerifier: true},
+			expectErr:         true,
+			errContent:        nil,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			_, err := getVerifierService(tt.conf, tt.pluginDir, tt.RevocationFactory)
+			if (err != nil) != tt.expectErr {
+				t.Errorf("error = %v, expectErr = %v", err, tt.expectErr)
+			}
+			if tt.errContent != nil && err.Error() != tt.errContent.Error() {
+				t.Errorf("error = %v, expectErr = %v, content = %v", err, tt.expectErr, tt.errContent)
+			}
+		})
+	}
+}
+
+type mockRevocationFactory struct {
+	failFetcher               bool
+	failFileCache             bool
+	failCodeSigningValidator  bool
+	failTimestampingValidator bool
+	failVerifier              bool
+	httpClient                *http.Client
+}
+
+func (m mockRevocationFactory) NewFetcher() (corecrl.Fetcher, error) {
+	if m.failFetcher {
+		return nil, fmt.Errorf("failed to create fetcher")
+	}
+	return corecrl.NewHTTPFetcher(m.httpClient)
+}
+
+func (m mockRevocationFactory) NewValidator(opts revocation.Options) (revocation.Validator, error) {
+	if m.failCodeSigningValidator && opts.CertChainPurpose == purpose.CodeSigning {
+		return nil, fmt.Errorf("failed to create code signing validator")
+	}
+	if m.failTimestampingValidator && opts.CertChainPurpose == purpose.Timestamping {
+		return nil, fmt.Errorf("failed to create timestamping validator")
+	}
+	return revocation.NewWithOptions(opts)
+}
diff --git a/pkg/verifier/notation/notationrevocationfactory_test.go b/pkg/verifier/notation/notationrevocationfactory_test.go
new file mode 100644
index 0000000..b5355f8
--- /dev/null
+++ b/pkg/verifier/notation/notationrevocationfactory_test.go
@@ -0,0 +1,103 @@
+// 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 (
+	"net/http"
+	"runtime"
+	"testing"
+
+	"github.com/notaryproject/notation-core-go/revocation"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestNewRevocationFactoryImpl(t *testing.T) {
+	factory := NewRevocationFactoryImpl()
+	assert.NotNil(t, factory)
+}
+
+func TestNewFetcher(t *testing.T) {
+	tests := []struct {
+		name       string
+		cacheRoot  string
+		httpClient *http.Client
+		wantErr    bool
+	}{
+		{
+			name:       "valid fetcher",
+			cacheRoot:  "/valid/path",
+			httpClient: &http.Client{},
+			wantErr:    false,
+		},
+		{
+			name:       "invalid fetcher with nil httpClient",
+			cacheRoot:  "/valid/path",
+			httpClient: nil,
+			wantErr:    true,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			factory := &RevocationFactoryImpl{
+				cacheRoot:  tt.cacheRoot,
+				httpClient: tt.httpClient,
+			}
+
+			fetcher, err := factory.NewFetcher()
+			if tt.wantErr {
+				assert.Error(t, err)
+				assert.Nil(t, fetcher)
+			}
+		})
+	}
+}
+
+func TestNewValidator(t *testing.T) {
+	factory := &RevocationFactoryImpl{}
+	opts := revocation.Options{}
+
+	validator, err := factory.NewValidator(opts)
+	assert.NoError(t, err)
+	assert.NotNil(t, validator)
+}
+func TestNewFileCache(t *testing.T) {
+	tests := []struct {
+		name      string
+		cacheRoot string
+		wantErr   bool
+	}{
+		{
+			name:      "valid cache root",
+			cacheRoot: "/valid/path",
+			wantErr:   false,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if runtime.GOOS == "windows" {
+				t.Skip("skipping test on Windows")
+			}
+			cache, err := newFileCache(tt.cacheRoot)
+			if tt.wantErr {
+				assert.Error(t, err)
+				assert.Nil(t, cache)
+			} else {
+				assert.NoError(t, err)
+				assert.NotNil(t, cache)
+			}
+		})
+	}
+}