diff --git a/config/base.go b/config/base.go index a157db33..6793645a 100644 --- a/config/base.go +++ b/config/base.go @@ -16,8 +16,8 @@ var ( SigningKeysPath string // configInfo is the information of config.json - configInfo *ConfigFile - configOnce sync.Once + configInfo *Config + configInfoOnce sync.Once // signingKeysInfo is the information of signingkeys.json signingKeysInfo *SigningKeys @@ -27,16 +27,43 @@ var ( func init() { ConfigPath = dir.Path.Config() SigningKeysPath = dir.Path.SigningKeyConfig() +} + +// Configuration is the main config struct of notation-go +type Configuration struct { + Config + SigningKeys +} +// Save stores sub-configurations to files +func (c *Configuration) Save() error { + if err := c.Config.Save(); err != nil { + return err + } + return c.SigningKeys.Save() } -// Configuration is a interface to manage notation config -type Configuration interface { - Save() error +// LoadOnce returns the previously read config file. +// If previous config file does not exist, it reads the config from file +// or return a default config if not found. +// The returned config is only suitable for read only scenarios for short-lived processes. +func LoadOnce() (*Configuration, error) { + configInfo, err := loadConfigOnce() + if err != nil { + return nil, err + } + signingKeysInfo, err := loadSigningKeysOnce() + if err != nil { + return nil, err + } + return &Configuration{ + Config: *configInfo, + SigningKeys: *signingKeysInfo, + }, nil } -// Save stores the config to file -func Save(filePath string, config interface{}) error { +// Save stores the cfg struct to file +func Save(filePath string, cfg interface{}) error { dir := filepath.Dir(filePath) if err := os.MkdirAll(dir, 0700); err != nil { return err @@ -48,15 +75,15 @@ func Save(filePath string, config interface{}) error { defer file.Close() encoder := json.NewEncoder(file) encoder.SetIndent("", " ") - return encoder.Encode(config) + return encoder.Encode(cfg) } -// Load reads the config from file -func Load(filePath string, config interface{}) error { +// Load reads file, parses json and stores in cfg struct +func Load(filePath string, cfg interface{}) error { file, err := os.Open(filePath) if err != nil { return err } defer file.Close() - return json.NewDecoder(file).Decode(config) + return json.NewDecoder(file).Decode(cfg) } diff --git a/config/base_test.go b/config/base_test.go new file mode 100644 index 00000000..fde9cac4 --- /dev/null +++ b/config/base_test.go @@ -0,0 +1,56 @@ +package config + +import ( + "fmt" + "path/filepath" + "reflect" + "testing" + + "github.com/notaryproject/notation-go/dir" +) + +func TestLoadOnce(t *testing.T) { + t.Cleanup(func() { + ConfigPath = dir.Path.Config() + SigningKeysPath = dir.Path.SigningKeyConfig() + }) + ConfigPath = configPath + SigningKeysPath = signingKeysPath + cfg, err := LoadOnce() + if err != nil { + t.Fatal("call LoadOnce() failed.") + } + if reflect.DeepEqual(Configuration{ + Config: *sampleConfig, + SigningKeys: *sampleSigningKeysInfo, + }, cfg) { + t.Fatal("call LoadOnce() failed.") + } +} + +func TestOnce(t *testing.T) { + t.Cleanup(func() { + ConfigPath = dir.Path.Config() + SigningKeysPath = dir.Path.SigningKeyConfig() + }) + root := t.TempDir() + ConfigPath = filepath.Join(root, dir.ConfigFile) + SigningKeysPath = filepath.Join(root, dir.SigningKeysFile) + cfg := Configuration{ + Config: *sampleConfig, + SigningKeys: *sampleSigningKeysInfo, + } + // save config in temp directory + err := cfg.Save() + if err != nil { + t.Fatal(fmt.Sprintf("call Save() failed. error: %v", err)) + } + // load saved file + savedCfg, err := LoadOnce() + if err != nil { + t.Fatal("call LoadOnce() failed.") + } + if reflect.DeepEqual(cfg, savedCfg) { + t.Fatal("call Save() failed.") + } +} diff --git a/config/config.go b/config/config.go index ac21a9fb..0dcf2e5c 100644 --- a/config/config.go +++ b/config/config.go @@ -16,11 +16,13 @@ func (c CertificateReference) Is(name string) bool { return c.Name == name } -// ConfigFile reflects the config file. +// Config reflects the config file. // Specification: https://github.com/notaryproject/notation/pull/76 -type ConfigFile struct { +type Config struct { VerificationCertificates VerificationCertificates `json:"verificationCerts"` InsecureRegistries []string `json:"insecureRegistries"` + CredentialsStore string `json:"credsStore,omitempty"` + CredentialHelpers map[string]string `json:"credHelpers,omitempty"` } // VerificationCertificates is a collection of public certs used for verification. @@ -29,20 +31,20 @@ type VerificationCertificates struct { } // NewConfig creates a new config file -func NewConfig() *ConfigFile { - return &ConfigFile{ +func NewConfig() *Config { + return &Config{ InsecureRegistries: []string{}, } } // Save stores the config to file -func (f *ConfigFile) Save() error { - return Save(ConfigPath, f) +func (c *Config) Save() error { + return Save(ConfigPath, c) } -// LoadConfig reads the config from file or return a default config if not found. -func LoadConfig() (*ConfigFile, error) { - var config ConfigFile +// loadConfig reads the config from file or return a default config if not found. +func loadConfig() (*Config, error) { + var config Config err := Load(ConfigPath, &config) if err != nil { if errors.Is(err, fs.ErrNotExist) { @@ -53,14 +55,14 @@ func LoadConfig() (*ConfigFile, error) { return &config, nil } -// LoadConfigOnce returns the previously read config file. -// If previous config file does not exists, it reads the config from file +// loadConfigOnce returns the previously read config file. +// If previous config file does not exist, it reads the config from file // or return a default config if not found. // The returned config is only suitable for read only scenarios for short-lived processes. -func LoadConfigOnce() (*ConfigFile, error) { +func loadConfigOnce() (*Config, error) { var err error - configOnce.Do(func() { - configInfo, err = LoadConfig() + configInfoOnce.Do(func() { + configInfo, err = loadConfig() }) return configInfo, err } diff --git a/config/config_test.go b/config/config_test.go index 6d6f1acd..9df63066 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -13,7 +13,7 @@ const ( nonexistentPath = "./testdata/nonexistent.json" ) -var sampleConfig = &ConfigFile{ +var sampleConfig = &Config{ VerificationCertificates: VerificationCertificates{ Certificates: []CertificateReference{ { @@ -31,7 +31,7 @@ var sampleConfig = &ConfigFile{ }, } -func TestLoadConfig(t *testing.T) { +func TestLoadFile(t *testing.T) { t.Cleanup(func() { // restore path ConfigPath = dir.Path.Config() @@ -42,7 +42,7 @@ func TestLoadConfig(t *testing.T) { tests := []struct { name string args args - want *ConfigFile + want *Config wantErr bool }{ { @@ -61,19 +61,19 @@ func TestLoadConfig(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ConfigPath = tt.args.filePath - got, err := LoadConfig() + got, err := loadConfig() if (err != nil) != tt.wantErr { - t.Errorf("LoadConfig() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("loadFile() error = %v, wantErr %v", err, tt.wantErr) return } if !reflect.DeepEqual(got, tt.want) { - t.Errorf("LoadConfig() = %v, want %v", got, tt.want) + t.Errorf("loadFile() = %v, want %v", got, tt.want) } }) } } -func TestSaveConfigFile(t *testing.T) { +func TestSaveFile(t *testing.T) { t.Cleanup(func() { // restore path ConfigPath = dir.Path.Config() @@ -81,7 +81,7 @@ func TestSaveConfigFile(t *testing.T) { root := t.TempDir() ConfigPath = filepath.Join(root, "config.json") sampleConfig.Save() - config, err := LoadConfig() + config, err := loadConfig() if err != nil { t.Fatal("Load config file from temp dir failed") } diff --git a/config/keys.go b/config/keys.go index d6ebd44b..1325939a 100644 --- a/config/keys.go +++ b/config/keys.go @@ -48,9 +48,9 @@ func NewSigningKeys() *SigningKeys { return &SigningKeys{Keys: []KeySuite{}} } -// LoadSigningKeys reads the config from file +// loadSigningKeys reads the config from file // or return a default config if not found. -func LoadSigningKeys() (*SigningKeys, error) { +func loadSigningKeys() (*SigningKeys, error) { var config SigningKeys err := Load(SigningKeysPath, &config) if err != nil { @@ -62,14 +62,14 @@ func LoadSigningKeys() (*SigningKeys, error) { return &config, nil } -// LoadSigningKeysOnce returns the previously read config file. -// If previous config file does not exists, it reads the config from file +// loadSigningKeysOnce returns the previously read config file. +// If previous config file does not exist, it reads the config from file // or return a default config if not found. // The returned config is only suitable for read only scenarios for short-lived processes. -func LoadSigningKeysOnce() (*SigningKeys, error) { +func loadSigningKeysOnce() (*SigningKeys, error) { var err error signingKeysInfoOnce.Do(func() { - signingKeysInfo, err = LoadSigningKeys() + signingKeysInfo, err = loadSigningKeys() }) return signingKeysInfo, err } diff --git a/config/keys_test.go b/config/keys_test.go index 00352c7d..b6405272 100644 --- a/config/keys_test.go +++ b/config/keys_test.go @@ -70,7 +70,7 @@ func TestLoadSigningKeysInfo(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { SigningKeysPath = tt.args.filePath - got, err := LoadSigningKeys() + got, err := loadSigningKeys() if err != nil { t.Errorf("LoadSigningKeysInfo() error = %v", err) return @@ -90,7 +90,7 @@ func TestSaveSigningKeys(t *testing.T) { root := t.TempDir() SigningKeysPath = filepath.Join(root, "signingkeys.json") sampleSigningKeysInfo.Save() - info, err := LoadSigningKeys() + info, err := loadSigningKeys() if err != nil { t.Fatal("Load signingkeys.json from temp dir failed.") } diff --git a/config/util.go b/config/util.go index 6ef309a2..4801c9f4 100644 --- a/config/util.go +++ b/config/util.go @@ -12,7 +12,7 @@ var ( // IsRegistryInsecure checks whether the registry is in the list of insecure registries. func IsRegistryInsecure(target string) bool { - config, err := LoadConfigOnce() + config, err := loadConfigOnce() if err != nil { return false } @@ -27,7 +27,7 @@ func IsRegistryInsecure(target string) bool { // ResolveKey resolves the key by name. // The default key is attempted if name is empty. func ResolveKey(name string) (KeySuite, error) { - config, err := LoadSigningKeysOnce() + config, err := loadSigningKeysOnce() if err != nil { return KeySuite{}, err } diff --git a/dir/fs.go b/dir/fs.go index 0616781a..f958f0fa 100644 --- a/dir/fs.go +++ b/dir/fs.go @@ -132,10 +132,14 @@ func (u unionDirFS) ReadDir(name string) ([]fs.DirEntry, error) { } // PluginFS returns the UnionDirFS for notation plugins +// if dirs is set, use dirs as the directories for plugins +// if dirs is not set, use build-in directory structure for plugins func PluginFS(dirs ...string) UnionDirFS { var rootedFsys []RootedFS - dirs = append(dirs, filepath.Join(userLibexec, "plugins")) - dirs = append(dirs, filepath.Join(systemLibexec, "plugins")) + if len(dirs) == 0 { + dirs = append(dirs, filepath.Join(userLibexec, "plugins")) + dirs = append(dirs, filepath.Join(systemLibexec, "plugins")) + } for _, dir := range dirs { rootedFsys = append(rootedFsys, NewRootedFS(dir, nil)) } diff --git a/dir/fs_test.go b/dir/fs_test.go index 68b0352e..21e2c550 100644 --- a/dir/fs_test.go +++ b/dir/fs_test.go @@ -3,6 +3,8 @@ package dir import ( "errors" "io/fs" + "path/filepath" + "reflect" "testing" "testing/fstest" ) @@ -238,3 +240,37 @@ func TestPathTrustStore(t *testing.T) { }) } } + +func TestPluginFS(t *testing.T) { + type args struct { + dirs []string + } + tests := []struct { + name string + args args + want UnionDirFS + }{ + { + name: "default PluginFS", + args: args{dirs: []string{}}, + want: NewUnionDirFS( + NewRootedFS(filepath.Join(userLibexec, "plugins"), nil), + NewRootedFS(filepath.Join(systemLibexec, "plugins"), nil), + ), + }, + { + name: "custom PluginFS", + args: args{dirs: []string{"/home/user/plugins"}}, + want: NewUnionDirFS( + NewRootedFS("/home/user/plugins", nil), + ), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := PluginFS(tt.args.dirs...); !reflect.DeepEqual(got, tt.want) { + t.Errorf("PluginFS() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/dir/path.go b/dir/path.go index 85f2e009..42815077 100644 --- a/dir/path.go +++ b/dir/path.go @@ -2,22 +2,37 @@ package dir import ( "errors" + "fmt" "io/fs" "github.com/opencontainers/go-digest" ) const ( - // SignatureExtension defines the extension of the signature files - SignatureExtension = ".sig" + // CertificateExtension defines the extension of the certificate files + CertificateExtension = ".crt" + // ConfigFile is the name of config file ConfigFile = "config.json" + + // KeyExtension defines the extension of the key files + KeyExtension = ".key" + // LocalKeysDir is the directory name for local key store LocalKeysDir = "localkeys" + + // SignatureExtension defines the extension of the signature files + SignatureExtension = ".sig" + + // SignatureStoreDirName is the name of the signature store directory + SignatureStoreDirName = "signatures" + // SigningKeysFile is the file name of signing key info SigningKeysFile = "signingkeys.json" + // TrustPolicyFile is the file name of trust policy info TrustPolicyFile = "trustpolicy.json" + // TrustStoreDir is the directory name of trust store TrustStoreDir = "truststore" ) @@ -31,56 +46,81 @@ type PathManager struct { UserConfigFS UnionDirFS } -func errorHandler(path string, err error) string { - // if path does not exist, return path for creating file. +func checkError(err error) { + // if path does not exist, the path can be used to create file if err != nil && !errors.Is(err, fs.ErrNotExist) { panic(err) } - return path } // Config returns the path of config.json func (p *PathManager) Config() string { - return errorHandler(p.ConfigFS.GetPath(ConfigFile)) + path, err := p.ConfigFS.GetPath(ConfigFile) + checkError(err) + return path } // LocalKey returns path of the local private keys or certificate // in the localkeys directory -func (p *PathManager) Localkey(keyName string) string { - return errorHandler(p.UserConfigFS.GetPath(LocalKeysDir, keyName)) +// +// extension: support .crt|.key +func (p *PathManager) Localkey(name string, extension string) string { + if extension != KeyExtension && extension != CertificateExtension { + panic(fmt.Sprintf("doesn't support the extension `%s`", extension)) + } + path, err := p.UserConfigFS.GetPath(LocalKeysDir, name+extension) + checkError(err) + return path } // SigningKeyConfig return the path of signingkeys.json files func (p *PathManager) SigningKeyConfig() string { - return errorHandler(p.UserConfigFS.GetPath(SigningKeysFile)) + path, err := p.UserConfigFS.GetPath(SigningKeysFile) + checkError(err) + return path } // TrustPolicy returns the path of trustpolicy.json file func (p *PathManager) TrustPolicy() string { - return errorHandler(p.ConfigFS.GetPath(TrustPolicyFile)) + path, err := p.ConfigFS.GetPath(TrustPolicyFile) + checkError(err) + return path } // X509TrustStore returns the path of x509 trust store certificate func (p *PathManager) X509TrustStore(prefix, namedStore string) string { - return errorHandler(p.ConfigFS.GetPath(TrustStoreDir, "x509", prefix, namedStore)) + path, err := p.ConfigFS.GetPath(TrustStoreDir, "x509", prefix, namedStore) + checkError(err) + return path } // CachedSignature returns the cached signature file path func (p *PathManager) CachedSignature(manifestDigest, signatureDigest digest.Digest) string { - return errorHandler(p.CacheFS.GetPath( - "signatures", + path, err := p.CacheFS.GetPath( + SignatureStoreDirName, manifestDigest.Algorithm().String(), manifestDigest.Encoded(), signatureDigest.Algorithm().String(), signatureDigest.Encoded()+SignatureExtension, - )) + ) + checkError(err) + return path } // CachedSignatureRoot returns the cached signature root path func (p *PathManager) CachedSignatureRoot(manifestDigest digest.Digest) string { - return errorHandler(p.CacheFS.GetPath( - "signatures", + path, err := p.CacheFS.GetPath( + SignatureStoreDirName, manifestDigest.Algorithm().String(), manifestDigest.Encoded(), - )) + ) + checkError(err) + return path +} + +// CachedSignatureStoreDirPath returns the cached signing keys directory +func (p *PathManager) CachedSignatureStoreDirPath() string { + path, err := p.CacheFS.GetPath(SignatureStoreDirName) + checkError(err) + return path } diff --git a/dir/path_test.go b/dir/path_test.go index dda99ee8..f7438fb9 100644 --- a/dir/path_test.go +++ b/dir/path_test.go @@ -118,3 +118,113 @@ func TestX509TrustStoreCerts(t *testing.T) { }) } } + +func TestPathManager_Config(t *testing.T) { + path := &PathManager{ + ConfigFS: NewUnionDirFS( + NewRootedFS("/home/exampleuser/.config/notation/", nil), + ), + } + configPath := path.Config() + if configPath != "/home/exampleuser/.config/notation/"+ConfigFile { + t.Fatal("get Config() failed.") + } +} + +func TestPathManager_LocalKey(t *testing.T) { + path := &PathManager{ + UserConfigFS: NewUnionDirFS( + NewRootedFS("/home/exampleuser/.config/notation/", nil), + ), + } + localkeyPath := path.Localkey("key1", KeyExtension) + if localkeyPath != "/home/exampleuser/.config/notation/localkeys/key1"+KeyExtension { + t.Fatal("get Localkey() failed.") + } +} + +func TestPathManager_LocalKeyFailed(t *testing.T) { + path := &PathManager{ + UserConfigFS: NewUnionDirFS( + NewRootedFS("/home/exampleuser/.config/notation/", nil), + ), + } + defer func() { + if d := recover(); d == nil { + t.Fatal("get Localkey() extension check failed.") + } + }() + path.Localkey("key1", ".acr") +} + +func TestPathManager_SigningKeyConfig(t *testing.T) { + path := &PathManager{ + UserConfigFS: NewUnionDirFS( + NewRootedFS("/home/exampleuser/.config/notation/", nil), + ), + } + signingKeyPath := path.SigningKeyConfig() + if signingKeyPath != "/home/exampleuser/.config/notation/"+SigningKeysFile { + t.Fatal("get SigningKeyConfig() failed.") + } +} + +func TestPathManager_TrustPolicy(t *testing.T) { + path := &PathManager{ + ConfigFS: NewUnionDirFS( + NewRootedFS("/home/exampleuser/.config/notation/", nil), + ), + } + policyPath := path.TrustPolicy() + if policyPath != "/home/exampleuser/.config/notation/"+TrustPolicyFile { + t.Fatal("get TrustPolicy() failed.") + } +} + +func TestPathManager_X509TrustStore(t *testing.T) { + path := &PathManager{ + ConfigFS: NewUnionDirFS( + NewRootedFS("/home/exampleuser/.config/notation/", nil), + ), + } + storePath := path.X509TrustStore("ca", "store") + if storePath != "/home/exampleuser/.config/notation/truststore/x509/ca/store" { + t.Fatal("get X509TrustStore() failed.") + } +} + +func TestPathManager_CachedSignature(t *testing.T) { + path := &PathManager{ + CacheFS: NewUnionDirFS( + NewRootedFS("/home/exampleuser/.cache/notation/", nil), + ), + } + signaturePath := path.CachedSignature("sha256:x1", "sha256:x2") + if signaturePath != "/home/exampleuser/.cache/notation/signatures/sha256/x1/sha256/x2.sig" { + t.Fatal("get CachedSignature() failed.") + } +} + +func TestPathManager_CachedSignatureRoot(t *testing.T) { + path := &PathManager{ + CacheFS: NewUnionDirFS( + NewRootedFS("/home/exampleuser/.cache/notation/", nil), + ), + } + signaturePath := path.CachedSignatureRoot("sha256:x1") + if signaturePath != "/home/exampleuser/.cache/notation/signatures/sha256/x1" { + t.Fatal("get CachedSignatureRoot() failed.") + } +} + +func TestPathManager_CachedSignatureStoreDirPath(t *testing.T) { + path := &PathManager{ + CacheFS: NewUnionDirFS( + NewRootedFS("/home/exampleuser/.cache/notation/", nil), + ), + } + signatureDirPath := path.CachedSignatureStoreDirPath() + if signatureDirPath != "/home/exampleuser/.cache/notation/signatures" { + t.Fatal("get CachedSignatureStoreDir() failed.") + } +} diff --git a/plugin/plugin.go b/plugin/plugin.go index 76c420c9..35bbdc1f 100644 --- a/plugin/plugin.go +++ b/plugin/plugin.go @@ -122,12 +122,12 @@ type DescribeKeyResponse struct { // GenerateSignatureRequest contains the parameters passed in a generate-signature request. type GenerateSignatureRequest struct { - ContractVersion string `json:"contractVersion"` - KeyID string `json:"keyId"` - KeySpec signer.KeySpec `json:"keySpec"` - Hash string `json:"hashAlgorithm"` - Payload []byte `json:"payload"` - PluginConfig map[string]string `json:"pluginConfig,omitempty"` + ContractVersion string `json:"contractVersion"` + KeyID string `json:"keyId"` + KeySpec signer.KeySpec `json:"keySpec"` + Hash string `json:"hashAlgorithm"` + Payload []byte `json:"payload"` + PluginConfig map[string]string `json:"pluginConfig,omitempty"` } func (GenerateSignatureRequest) Command() Command { @@ -136,9 +136,9 @@ func (GenerateSignatureRequest) Command() Command { // GenerateSignatureResponse is the response of a generate-signature request. type GenerateSignatureResponse struct { - KeyID string `json:"keyId"` - Signature []byte `json:"signature"` - SigningAlgorithm signer.SignatureAlgorithm `json:"signingAlgorithm"` + KeyID string `json:"keyId"` + Signature []byte `json:"signature"` + SigningAlgorithm signer.SignatureAlgorithm `json:"signingAlgorithm"` // Ordered list of certificates starting with leaf certificate // and ending with root certificate.