Repository: notaryproject/notation (go)

Issue:
Load config issue
### What is not working as expected?

```
➜ chmod 0000 ~/.config/notation 
➜ notation 
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x38 pc=0x850078]

goroutine 1 [running]:
github.com/notaryproject/notation/internal/cmd.init.func2(0xc0000e2900, 0xc0000d64c8)
        /home/jj/projects/junjie/notation/internal/cmd/flags.go:50 +0x38
github.com/notaryproject/notation/internal/cmd.(*SignerFlagOpts).ApplyFlagsToCommand(0xc0000d64b8, 0xc00021c008)
        /home/jj/projects/junjie/notation/internal/cmd/options.go:39 +0x5a
main.signCommand(0xc000076698?)
        /home/jj/projects/junjie/notation/cmd/notation/sign.go:144 +0x1d6
main.main()
        /home/jj/projects/junjie/notation/cmd/notation/main.go:56 +0x79
```
When the notation config directory lacks permission, it will trigger an error in `LoadConfigOnce()`. This issue was exposed after the blob signing command was added, which required calling `LoadConfigOnce()` twice (OCI signing + blob signing command). We need to fix it.

### What did you expect to happen?

No error.

### How can we reproduce it?

run ```chmod 0000 ~/.config/notation```, then call notation command

### Describe your environment

Linux x64

### What is the version of your Notation CLI or Notation Library?

`main` branch

Pull request:
fix: load config error
Fix:
- the `LoadConfigOnce` function forgets the error and return a nil config and nil error next time. Updated to use `sync.OnceValues` to keep the returned values.

Resolves #1144 

Patch:
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index 980c79f..43530df 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -49,8 +49,8 @@ jobs:
           go-version: ${{ matrix.go-version }}
           check-latest: true
       - name: Initialize CodeQL
-        uses: github/codeql-action/init@48ab28a6f5dbc2a99bf1e0131198dd8f1df78169 # v3.28.0
+        uses: github/codeql-action/init@b6a472f63d85b9c78a3ac5e89422239fc15e9b3c # v3.28.1
         with:
           languages: go
       - name: Perform CodeQL Analysis
-        uses: github/codeql-action/analyze@48ab28a6f5dbc2a99bf1e0131198dd8f1df78169 # v3.28.0
+        uses: github/codeql-action/analyze@b6a472f63d85b9c78a3ac5e89422239fc15e9b3c # v3.28.1
diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml
index e7219e3..10e494b 100644
--- a/.github/workflows/scorecard.yml
+++ b/.github/workflows/scorecard.yml
@@ -61,6 +61,6 @@ jobs:
           retention-days: 5
       
       - name: "Upload to code-scanning"
-        uses: github/codeql-action/upload-sarif@48ab28a6f5dbc2a99bf1e0131198dd8f1df78169 # v3.28.0
+        uses: github/codeql-action/upload-sarif@b6a472f63d85b9c78a3ac5e89422239fc15e9b3c # v3.28.1
         with:
           sarif_file: results.sarif
diff --git a/go.mod b/go.mod
index 8884346..320b0a4 100644
--- a/go.mod
+++ b/go.mod
@@ -5,7 +5,7 @@ go 1.23
 require (
 	github.com/notaryproject/notation-core-go v1.2.0-rc.2
 	github.com/notaryproject/notation-go v1.2.0-beta.1.0.20250107003620-26ce0894a624
-	github.com/notaryproject/tspclient-go v1.0.0-rc.1
+	github.com/notaryproject/tspclient-go v1.0.0
 	github.com/opencontainers/go-digest v1.0.0
 	github.com/opencontainers/image-spec v1.1.0
 	github.com/sirupsen/logrus v1.9.3
diff --git a/go.sum b/go.sum
index 34a5fdd..5e0aade 100644
--- a/go.sum
+++ b/go.sum
@@ -42,8 +42,8 @@ github.com/notaryproject/notation-go v1.2.0-beta.1.0.20250107003620-26ce0894a624
 github.com/notaryproject/notation-go v1.2.0-beta.1.0.20250107003620-26ce0894a624/go.mod h1:1QaHYG/UOeAYhfLBipsSxquu3BheRm7a+5RODcc5nQg=
 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 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/notaryproject/tspclient-go v1.0.0 h1:AwQ4x0gX8IHnyiZB1tggpn5NFqHpTEm1SDX8YNv4Dg4=
+github.com/notaryproject/tspclient-go v1.0.0/go.mod h1:LGyA/6Kwd2FlM0uk8Vc5il3j0CddbWSHBj/4kxQDbjs=
 github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
 github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
diff --git a/internal/cmd/flags.go b/internal/cmd/flags.go
index 196768b..a2bed99 100644
--- a/internal/cmd/flags.go
+++ b/internal/cmd/flags.go
@@ -44,14 +44,14 @@ var (
 		Usage: "signature envelope format, options: \"jws\", \"cose\"",
 	}
 	SetPflagSignatureFormat = func(fs *pflag.FlagSet, p *string) {
-		defaultSignatureFormat := envelope.JWS
-		// load config to get signatureFormat
 		config, err := configutil.LoadConfigOnce()
-		if err == nil && config.SignatureFormat != "" {
-			defaultSignatureFormat = config.SignatureFormat
+		if err != nil || config.SignatureFormat == "" {
+			fs.StringVar(p, PflagSignatureFormat.Name, envelope.JWS, PflagSignatureFormat.Usage)
+			return
 		}
 
-		fs.StringVar(p, PflagSignatureFormat.Name, defaultSignatureFormat, PflagSignatureFormat.Usage)
+		// set signatureFormat from config
+		fs.StringVar(p, PflagSignatureFormat.Name, config.SignatureFormat, PflagSignatureFormat.Usage)
 	}
 
 	PflagID = &pflag.Flag{
diff --git a/pkg/configutil/once.go b/pkg/configutil/once.go
index 00e2a2a..f5e1d96 100644
--- a/pkg/configutil/once.go
+++ b/pkg/configutil/once.go
@@ -21,28 +21,28 @@ import (
 	"github.com/notaryproject/notation/internal/envelope"
 )
 
-var (
-	// configInfo is the config.json data
-	configInfo *config.Config
-	configOnce sync.Once
-)
+// loadConfigOnce is a function that invokes loadConfig only once.
+var loadConfigOnce = sync.OnceValues(loadConfig)
 
 // 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() (*config.Config, error) {
-	var err error
-	configOnce.Do(func() {
-		configInfo, err = config.LoadConfig()
-		if err != nil {
-			return
-		}
-		// set default value
-		configInfo.SignatureFormat = strings.ToLower(configInfo.SignatureFormat)
-		if configInfo.SignatureFormat == "" {
-			configInfo.SignatureFormat = envelope.JWS
-		}
-	})
-	return configInfo, err
+	return loadConfigOnce()
+}
+
+// loadConfig reads the config from file or return a default config if not
+// found.
+func loadConfig() (*config.Config, error) {
+	configInfo, err := config.LoadConfig()
+	if err != nil {
+		return nil, err
+	}
+	// set default value
+	configInfo.SignatureFormat = strings.ToLower(configInfo.SignatureFormat)
+	if configInfo.SignatureFormat == "" {
+		configInfo.SignatureFormat = envelope.JWS
+	}
+	return configInfo, nil
 }


Test patch:
diff --git a/pkg/configutil/once_test.go b/pkg/configutil/once_test.go
index d218669..bd40691 100644
--- a/pkg/configutil/once_test.go
+++ b/pkg/configutil/once_test.go
@@ -14,10 +14,19 @@
 package configutil
 
 import (
+	"os"
+	"path/filepath"
+	"strings"
+	"sync"
 	"testing"
+
+	"github.com/notaryproject/notation-go/dir"
 )
 
 func TestLoadConfigOnce(t *testing.T) {
+	defer func() {
+		loadConfigOnce = sync.OnceValues(loadConfig)
+	}()
 	config1, err := LoadConfigOnce()
 	if err != nil {
 		t.Fatal("LoadConfigOnce failed.")
@@ -27,6 +36,26 @@ func TestLoadConfigOnce(t *testing.T) {
 		t.Fatal("LoadConfigOnce failed.")
 	}
 	if config1 != config2 {
-		t.Fatal("LoadConfigOnce is invalid.")
+		t.Fatal("LoadConfigOnce should return the same config.")
+	}
+}
+
+func TestLoadConfigOnceError(t *testing.T) {
+	dir.UserConfigDir = t.TempDir()
+	defer func() {
+		dir.UserConfigDir = ""
+		loadConfigOnce = sync.OnceValues(loadConfig)
+	}()
+	if err := os.WriteFile(filepath.Join(dir.UserConfigDir, dir.PathConfigFile), []byte("invalid json"), 0600); err != nil {
+		t.Fatal("Failed to create file.")
+	}
+
+	_, err := LoadConfigOnce()
+	if err == nil || !strings.Contains(err.Error(), "invalid character") {
+		t.Fatal("LoadConfigOnce should fail.")
+	}
+	_, err2 := LoadConfigOnce()
+	if err != err2 {
+		t.Fatal("LoadConfigOnce should return the same error.")
 	}
 }
diff --git a/pkg/configutil/util_test.go b/pkg/configutil/util_test.go
index cac8758..1570996 100644
--- a/pkg/configutil/util_test.go
+++ b/pkg/configutil/util_test.go
@@ -25,11 +25,10 @@ import (
 )
 
 func TestIsRegistryInsecure(t *testing.T) {
-	configOnce = sync.Once{}
 	// for restore dir
 	defer func(oldDir string) {
 		dir.UserConfigDir = oldDir
-		configOnce = sync.Once{}
+		loadConfigOnce = sync.OnceValues(loadConfig)
 	}(dir.UserConfigDir)
 	// update config dir
 	dir.UserConfigDir = "testdata"
@@ -56,11 +55,10 @@ func TestIsRegistryInsecure(t *testing.T) {
 }
 
 func TestIsRegistryInsecureMissingConfig(t *testing.T) {
-	configOnce = sync.Once{}
 	// for restore dir
 	defer func(oldDir string) {
 		dir.UserConfigDir = oldDir
-		configOnce = sync.Once{}
+		loadConfigOnce = sync.OnceValues(loadConfig)
 	}(dir.UserConfigDir)
 	// update config dir
 	dir.UserConfigDir = "./testdata2"
@@ -93,7 +91,7 @@ func TestIsRegistryInsecureConfigPermissionError(t *testing.T) {
 	defer func(oldDir string) error {
 		// restore permission
 		dir.UserConfigDir = oldDir
-		configOnce = sync.Once{}
+		loadConfigOnce = sync.OnceValues(loadConfig)
 		return os.Chmod(filepath.Join(configDir, "config.json"), 0644)
 	}(dir.UserConfigDir)
 
@@ -113,6 +111,7 @@ func TestIsRegistryInsecureConfigPermissionError(t *testing.T) {
 func TestResolveKey(t *testing.T) {
 	defer func(oldDir string) {
 		dir.UserConfigDir = oldDir
+		loadConfigOnce = sync.OnceValues(loadConfig)
 	}(dir.UserConfigDir)
 
 	t.Run("valid e2e key", func(t *testing.T) {