Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: crl cache #462

Merged
merged 30 commits into from
Sep 26, 2024
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions dir/fs.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,8 @@ func ConfigFS() SysFS {
func PluginFS() SysFS {
return NewSysFS(filepath.Join(userLibexecDirPath(), PathPlugins))
}

// CacheFS is the cache SysFS
func CacheFS() SysFS {
return NewSysFS(userCacheDirPath())
}
Two-Hearts marked this conversation as resolved.
Show resolved Hide resolved
11 changes: 11 additions & 0 deletions dir/fs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,14 @@ func TestPluginFS(t *testing.T) {
t.Fatalf(`SysPath() failed. got: %q, want: %q`, path, filepath.Join(userLibexecDirPath(), PathPlugins, "plugin"))
}
}

func TestCacheFS(t *testing.T) {
cacheFS := CacheFS()
path, err := cacheFS.SysPath()
if err != nil {
t.Fatalf("SysPath() failed. err = %v", err)
}
if path != filepath.Join(UserCacheDir) {
t.Fatalf(`SysPath() failed. got: %q, want: %q`, path, UserConfigDir)
}
}
27 changes: 24 additions & 3 deletions dir/path.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
// limitations under the License.

// Package dir implements Notation directory structure.
// [directory spec]: https://github.com/notaryproject/notation/blob/main/specs/directory.md
// [directory spec]: https://notaryproject.dev/docs/user-guides/how-to/directory-structure/
//
// Example:
//
Expand All @@ -31,7 +31,7 @@
// - Set custom configurations directory:
// dir.UserConfigDir = '/path/to/configurations/'
//
// Only user level directory is supported for RC.1, and system level directory
// Only user level directory is supported, and system level directory
// may be added later.
package dir

Expand All @@ -44,6 +44,7 @@ import (
var (
UserConfigDir string // Absolute path of user level {NOTATION_CONFIG}
UserLibexecDir string // Absolute path of user level {NOTATION_LIBEXEC}
UserCacheDir string // Absolute path of user level {NOTATION_CACHE}
)

const (
Expand Down Expand Up @@ -77,7 +78,12 @@ const (
TrustStoreDir = "truststore"
)

var userConfigDir = os.UserConfigDir // for unit test
// for unit tests
var (
userConfigDir = os.UserConfigDir

userCacheDir = os.UserCacheDir
)

// userConfigDirPath returns the user level {NOTATION_CONFIG} path.
func userConfigDirPath() string {
Expand All @@ -103,6 +109,21 @@ func userLibexecDirPath() string {
return UserLibexecDir
}

// userCacheDirPath returns the user level {NOTATION_CACHE} path.
func userCacheDirPath() string {
if UserCacheDir == "" {
userDir, err := userCacheDir()
if err != nil {
// fallback to current directory
UserCacheDir = filepath.Join("."+notation, "cache")
return UserCacheDir
}
// set user cache
UserCacheDir = filepath.Join(userDir, notation)
}
return UserCacheDir
}

// LocalKeyPath returns the local key and local cert relative paths.
func LocalKeyPath(name string) (keyPath, certPath string) {
basePath := path.Join(LocalKeysDir, name)
Expand Down
29 changes: 23 additions & 6 deletions dir/path_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,22 @@ package dir

import (
"os"
"path/filepath"
"testing"
)

func mockGetUserConfig() (string, error) {
func mockUserPath() (string, error) {
return "/path/", nil
}

func setup() {
UserConfigDir = ""
UserLibexecDir = ""
UserCacheDir = ""
}

func Test_UserConfigDirPath(t *testing.T) {
userConfigDir = mockGetUserConfig
userConfigDir = mockUserPath
setup()
got := userConfigDirPath()
if got != "/path/notation" {
Expand All @@ -39,25 +41,40 @@ func Test_UserConfigDirPath(t *testing.T) {
func Test_NoHomeVariable(t *testing.T) {
t.Setenv("HOME", "")
t.Setenv("XDG_CONFIG_HOME", "")
t.Setenv("XDG_CACHE_HOME", "")
setup()
userConfigDir = os.UserConfigDir
got := userConfigDirPath()
if got != ".notation" {
t.Fatalf(`UserConfigDirPath() = %q, want ".notation"`, UserConfigDir)
t.Fatalf(`userConfigDirPath() = %q, want ".notation"`, got)
}
got = userCacheDirPath()
want := filepath.Join("."+notation, "cache")
if got != want {
t.Fatalf(`userCacheDirPath() = %q, want %q`, got, want)
}
}

func Test_UserLibexecDirPath(t *testing.T) {
userConfigDir = mockGetUserConfig
userConfigDir = mockUserPath
setup()
got := userLibexecDirPath()
if got != "/path/notation" {
t.Fatalf(`UserConfigDirPath() = %q, want "/path/notation"`, got)
}
}

func Test_UserCacheDirPath(t *testing.T) {
userCacheDir = mockUserPath
setup()
got := userCacheDirPath()
if got != "/path/notation" {
t.Fatalf(`UserCacheDirPath() = %q, want "/path/notation"`, got)
}
}

func TestLocalKeyPath(t *testing.T) {
userConfigDir = mockGetUserConfig
userConfigDir = mockUserPath
setup()
_ = userConfigDirPath()
_ = userLibexecDirPath()
Expand All @@ -71,7 +88,7 @@ func TestLocalKeyPath(t *testing.T) {
}

func TestX509TrustStoreDir(t *testing.T) {
userConfigDir = mockGetUserConfig
userConfigDir = mockUserPath
setup()
_ = userConfigDirPath()
_ = userLibexecDirPath()
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ go 1.22.0

require (
github.com/go-ldap/ldap/v3 v3.4.8
github.com/notaryproject/notation-core-go v1.1.1-0.20240918011623-695ea0c1ad1f
github.com/notaryproject/notation-core-go v1.1.1-0.20240920045731-0786f51de737
github.com/notaryproject/notation-plugin-framework-go v1.0.0
github.com/notaryproject/tspclient-go v0.2.0
github.com/opencontainers/go-digest v1.0.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh6
github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
github.com/notaryproject/notation-core-go v1.1.1-0.20240918011623-695ea0c1ad1f h1:TmwJtM3AZ7iQ1LJEbHRPAMRw4hA52/AbVrllSVjCNP0=
github.com/notaryproject/notation-core-go v1.1.1-0.20240918011623-695ea0c1ad1f/go.mod h1:+6AOh41JPrnVLbW/19SJqdhVHwKgIINBO/np0e7nXJA=
github.com/notaryproject/notation-core-go v1.1.1-0.20240920045731-0786f51de737 h1:Hp93KBCABE29+6zdS0GTg0T1SXj6qGatJyN1JMvTQqk=
github.com/notaryproject/notation-core-go v1.1.1-0.20240920045731-0786f51de737/go.mod h1:b/70rA4OgOHlg0A7pb8zTWKJadFO6781zS3a37KHEJQ=
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=
Expand Down
185 changes: 185 additions & 0 deletions verifier/crl/crl.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
// Copyright The Notary Project 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 crl provides functionalities for crl revocation check.
package crl

import (
"context"
"crypto/sha256"
"crypto/x509"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io/fs"
"os"
"path/filepath"
"time"

corecrl "github.com/notaryproject/notation-core-go/revocation/crl"
Two-Hearts marked this conversation as resolved.
Show resolved Hide resolved
"github.com/notaryproject/notation-go/log"
)

const (
// tmpFileName is the prefix of the temporary file
tmpFileName = "notation-*"
)

// FileCache implements corecrl.Cache.
//
// Key: url of the CRL.
//
// Value: corecrl.Bundle.
//
// This cache builds on top of the UNIX file system to leverage the file system's
// atomic operations. The `rename` and `remove` operations will unlink the old
// file but keep the inode and file descriptor for existing processes to access
// the file. The old inode will be dereferenced when all processes close the old
// file descriptor. Additionally, the operations are proven to be atomic on
// UNIX-like platforms, so there is no need to handle file locking.
//
// NOTE: For Windows, the `open`, `rename` and `remove` operations need file
// locking to ensure atomicity. The current implementation does not handle
// file locking, so the concurrent write from multiple processes may be failed.
// Please do not use this cache in a multi-process environment on Windows.
type FileCache struct {
// root is the root directory of the cache
root string
}

// fileCacheContent is the actual content saved in a FileCache
type fileCacheContent struct {
// RawBaseCRL is baseCRL.Raw
RawBaseCRL []byte `json:"rawBaseCRL"`

// RawDeltaCRL is deltaCRL.Raw
RawDeltaCRL []byte `json:"rawDeltaCRL,omitempty"`
}
Two-Hearts marked this conversation as resolved.
Show resolved Hide resolved

// NewFileCache creates a FileCache with root as the root directory
func NewFileCache(root string) (*FileCache, error) {
if err := os.MkdirAll(root, 0700); err != nil {
return nil, fmt.Errorf("failed to create crl file cache: %w", err)
}
return &FileCache{
root: root,
}, nil
}

// Get retrieves CRL bundle from c given url as key. If the key does not exist
// or the content has expired, corecrl.ErrCacheMiss is returned.
func (c *FileCache) Get(ctx context.Context, url string) (*corecrl.Bundle, error) {
logger := log.GetLogger(ctx)
logger.Infof("Retrieving crl bundle from file cache with key %q ...", url)

// get content from file cache
f, err := os.Open(filepath.Join(c.root, c.fileName(url)))
Two-Hearts marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
logger.Infof("CRL file cache miss. Key %q does not exist", url)
Two-Hearts marked this conversation as resolved.
Show resolved Hide resolved
return nil, corecrl.ErrCacheMiss
}
return nil, fmt.Errorf("failed to get crl bundle from file cache with key %q: %w", url, err)
Two-Hearts marked this conversation as resolved.
Show resolved Hide resolved
}
defer f.Close()

// decode content to crl Bundle
var content fileCacheContent
err = json.NewDecoder(f).Decode(&content)
if err != nil {
return nil, fmt.Errorf("failed to decode file retrieved from file cache: %w", err)
Two-Hearts marked this conversation as resolved.
Show resolved Hide resolved
}
var bundle corecrl.Bundle
bundle.BaseCRL, err = x509.ParseRevocationList(content.RawBaseCRL)
if err != nil {
return nil, fmt.Errorf("failed to parse base CRL of file retrieved from file cache: %w", err)
}
if content.RawDeltaCRL != nil {
bundle.DeltaCRL, err = x509.ParseRevocationList(content.RawDeltaCRL)
if err != nil {
return nil, fmt.Errorf("failed to parse delta CRL of file retrieved from file cache: %w", err)
}
}

// check expiry
if err := checkExpiry(ctx, bundle.BaseCRL.NextUpdate); err != nil {
return nil, err
}
if bundle.DeltaCRL != nil {
if err := checkExpiry(ctx, bundle.DeltaCRL.NextUpdate); err != nil {
return nil, err
Two-Hearts marked this conversation as resolved.
Show resolved Hide resolved
}
}

return &bundle, nil
}

// Set stores the CRL bundle in c with url as key.
func (c *FileCache) Set(ctx context.Context, url string, bundle *corecrl.Bundle) error {
logger := log.GetLogger(ctx)
logger.Infof("Storing crl bundle to file cache with key %q ...", url)

// sanity check
if bundle == nil {
return errors.New("failed to store crl bundle in file cache: bundle cannot be nil")
Two-Hearts marked this conversation as resolved.
Show resolved Hide resolved
}
if bundle.BaseCRL == nil {
return errors.New("failed to store crl bundle in file cache: bundle BaseCRL cannot be nil")
}

// actual content to be saved in the cache
content := fileCacheContent{
RawBaseCRL: bundle.BaseCRL.Raw,
}
if bundle.DeltaCRL != nil {
content.RawDeltaCRL = bundle.DeltaCRL.Raw
}

// save content to tmp file
tmpFile, err := os.CreateTemp("", tmpFileName)
if err != nil {
return fmt.Errorf("failed to store crl bundle in file cache: failed to create temp file: %w", err)

Check warning on line 152 in verifier/crl/crl.go

View check run for this annotation

Codecov / codecov/patch

verifier/crl/crl.go#L152

Added line #L152 was not covered by tests
}
err = json.NewEncoder(tmpFile).Encode(content)
Two-Hearts marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return fmt.Errorf("failed to store crl bundle in file cache: failed to encode content: %w", err)

Check warning on line 156 in verifier/crl/crl.go

View check run for this annotation

Codecov / codecov/patch

verifier/crl/crl.go#L156

Added line #L156 was not covered by tests
}

// rename is atomic on UNIX-like platforms
err = os.Rename(tmpFile.Name(), filepath.Join(c.root, c.fileName(url)))
if err != nil {
return fmt.Errorf("failed to store crl bundle in file cache: %w", err)
}
return nil
}

// fileName returns the filename of the content stored in c
func (c *FileCache) fileName(url string) string {
Two-Hearts marked this conversation as resolved.
Show resolved Hide resolved
hash := sha256.Sum256([]byte(url))
return hex.EncodeToString(hash[:])
}

// checkExpiry returns nil when nextUpdate is bounded before current time
func checkExpiry(ctx context.Context, nextUpdate time.Time) error {
logger := log.GetLogger(ctx)

if nextUpdate.IsZero() {
return errors.New("crl bundle retrieved from file cache does not contain valid NextUpdate")
}
if time.Now().After(nextUpdate) {
logger.Infof("CRL bundle retrieved from file cache has expired at %s", nextUpdate)
return corecrl.ErrCacheMiss
}
return nil
}
Loading
Loading