Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c43eb26127 |
@@ -25,6 +25,11 @@ Bring the repo into policy compliance in one commit:
|
||||
|
||||
# Completed Steps
|
||||
|
||||
- 2026-10-03: A PGP unlocker whose metadata has no usable GPG key ID
|
||||
no longer panics: `GetID()` warns with the unlocker's directory and
|
||||
returns `pgp-unknown`. `ListUnlockers` skips, with a warning, an
|
||||
unlocker whose metadata file cannot be read or parsed instead of
|
||||
failing, so `secret unlocker list` still lists the others.
|
||||
- 2026-10-02: A plain `docker build .` builds again: the size tests
|
||||
skip a case that needs more locked memory than the process can
|
||||
lock, and run every case under `script/cibuild`. The image stamps the
|
||||
@@ -101,8 +106,6 @@ Bring the repo into policy compliance in one commit:
|
||||
- Timing attacks: bytes.Equal passphrase compare (cli/init.go:
|
||||
209-216); non-constant-time public key compare (vault.go:95-100).
|
||||
- High priority:
|
||||
- Return errors instead of panicking on corrupted metadata
|
||||
(pgpunlocker.go:116, keychainunlocker.go:141).
|
||||
- Secure temporary file handling and cleanup.
|
||||
- Print cobra usage only for argument errors, not internal
|
||||
failures.
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
// Unlocker List Tests
|
||||
//
|
||||
// Tests for `secret unlocker list` behavior when the unlockers.d directory
|
||||
// cannot be read while the listing is being rendered:
|
||||
// Tests for `secret unlocker list` behavior when the unlockers.d directory,
|
||||
// or an unlocker's metadata in it, cannot be read while the listing is
|
||||
// being rendered:
|
||||
//
|
||||
// - TestUnlockersListSkipsUnreadableUnlockersDir: an unreadable
|
||||
// unlockers.d yields no rows rather than rows bearing synthesized IDs.
|
||||
// - TestUnlockersListSkipsOnlyUnreadableEntries: a readable entry is
|
||||
// still listed, with its real ID and its current-unlocker marker,
|
||||
// when a later entry's scan fails.
|
||||
// - TestUnlockersListToleratesCorruptMetadata: one unlocker's corrupt
|
||||
// metadata does not stop the others from being listed.
|
||||
//
|
||||
// The listing resolves each unlocker's real ID by rescanning unlockers.d
|
||||
// after the vault has already enumerated it. If that rescan fails the ID
|
||||
@@ -227,3 +230,56 @@ func TestUnlockersListReadableEntriesAreListed(t *testing.T) {
|
||||
assert.True(t, unlockers[0].IsCurrent)
|
||||
assert.False(t, unlockers[1].IsCurrent)
|
||||
}
|
||||
|
||||
// TestUnlockersListToleratesCorruptMetadata asserts that one unlocker with
|
||||
// corrupt metadata does not stop the listing. Metadata that is not JSON
|
||||
// leaves that unlocker out; PGP metadata without a usable GPG key ID lists
|
||||
// it as "pgp-unknown". The healthy unlocker is listed with its real ID.
|
||||
func TestUnlockersListToleratesCorruptMetadata(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
healthyID := "pgp-" + listTestGPGKeyID + "A"
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
metadata string
|
||||
wantIDs []string
|
||||
}{
|
||||
{
|
||||
name: "not JSON",
|
||||
metadata: "not json",
|
||||
wantIDs: []string{healthyID},
|
||||
},
|
||||
{
|
||||
name: "GPG key ID of the wrong type",
|
||||
metadata: `{"type": "pgp", "gpgKeyId": 42}`,
|
||||
wantIDs: []string{healthyID, "pgp-unknown"},
|
||||
},
|
||||
{
|
||||
name: "GPG key ID missing",
|
||||
metadata: `{"type": "pgp"}`,
|
||||
wantIDs: []string{healthyID, "pgp-unknown"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
fs := newListTestVault(t, 2)
|
||||
metadataPath := filepath.Join(listTestStateDir, "vaults.d",
|
||||
listTestVaultName, listTestUnlockersDirName,
|
||||
listTestUnlockerDirTwo, listTestMetadataFileName)
|
||||
require.NoError(t, afero.WriteFile(
|
||||
fs, metadataPath, []byte(tt.metadata), listTestFilePerm,
|
||||
))
|
||||
|
||||
unlockers := listUnlockersJSON(t, fs)
|
||||
require.Len(t, unlockers, len(tt.wantIDs))
|
||||
|
||||
for i, wantID := range tt.wantIDs {
|
||||
assert.Equal(t, wantID, unlockers[i].ID)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,14 +155,18 @@ func (p *PGPUnlocker) GetDirectory() string {
|
||||
return p.Directory
|
||||
}
|
||||
|
||||
// GetID implements Unlocker interface - generates ID from GPG key ID
|
||||
// GetID implements Unlocker interface - generates ID from GPG key ID.
|
||||
// If the metadata has no usable GPG key ID, it warns with the unlocker's
|
||||
// directory and returns "pgp-unknown", so listing the other unlockers
|
||||
// still works.
|
||||
func (p *PGPUnlocker) GetID() string {
|
||||
// Generate ID using GPG key ID: pgp-<keyid>
|
||||
gpgKeyID, err := p.GetGPGKeyID()
|
||||
if err != nil {
|
||||
// The vault metadata is corrupt - this is a fatal error
|
||||
// We cannot continue with a fallback ID as that would mask data corruption
|
||||
panic(fmt.Sprintf("PGP unlocker metadata is corrupt or missing GPG key ID: %v", err))
|
||||
Warn("PGP unlocker metadata is corrupt or missing its GPG key ID",
|
||||
"directory", p.Directory, "error", err)
|
||||
|
||||
return "pgp-unknown"
|
||||
}
|
||||
|
||||
return "pgp-" + gpgKeyID
|
||||
@@ -197,6 +201,10 @@ func (p *PGPUnlocker) GetGPGKeyID() (string, error) {
|
||||
return "", fmt.Errorf("failed to parse PGP metadata: %w", err)
|
||||
}
|
||||
|
||||
if pgpMetadata.GPGKeyID == "" {
|
||||
return "", fmt.Errorf("PGP metadata: %w", errGPGKeyIDEmpty)
|
||||
}
|
||||
|
||||
return pgpMetadata.GPGKeyID, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -247,16 +247,20 @@ func (v *Vault) ListUnlockers() ([]UnlockerMetadata, error) {
|
||||
|
||||
metadataBytes, err := afero.ReadFile(v.fs, metadataPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(
|
||||
"failed to read metadata for unlocker %s: %w", file.Name(), err)
|
||||
secret.Warn("Skipping unlocker directory with unreadable metadata file",
|
||||
"directory", file.Name(), "error", err)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
var metadata UnlockerMetadata
|
||||
|
||||
err = json.Unmarshal(metadataBytes, &metadata)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(
|
||||
"failed to parse metadata for unlocker %s: %w", file.Name(), err)
|
||||
secret.Warn("Skipping unlocker directory with corrupt metadata file",
|
||||
"directory", file.Name(), "error", err)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
unlockers = append(unlockers, metadata)
|
||||
|
||||
Reference in New Issue
Block a user