sops icon indicating copy to clipboard operation
sops copied to clipboard

gpg-passphrase request errors when trying to prompt for age key passphrase

Open termaxima opened this issue 2 months ago • 9 comments

With a running gpg-agent, and a passphrase-protected key in ~/.config/sops/age/keys.txt, sops attempts to let the agent prompt for a passphrase, but it seems the request is malformed, here is the error :

age1k6akl9r65sr3hdyfmqrqek0d38k24cqtex3k45nuckam6g5kzy9ss7yugt: FAILED
    - | failed to create reader for decrypting sops data key with
      | age: failed to decrypt identity file: could not read
      | passphrase: gpg-agent passphrase request errored: ERR
      | 67109144 IPC parameter error <GPG Agent> - invalid length of
      | cacheID
      | . Did not find keys in locations
      | 'SOPS_AGE_SSH_PRIVATE_KEY_FILE',
      | '/home/termaxima/.ssh/id_rsa', 'SOPS_AGE_KEY',
      | 'SOPS_AGE_KEY_FILE', and 'SOPS_AGE_KEY_CMD'.

With gpg-agent, passphrase requests work perfectly well (although the lack of caching means the user is re-prompted for the same passphrase multiple times, if the protected key is not the first on the recipient list)

This was tested using sops 3.11.0 and gpg-agent 2.4.8 (libcrypt 1.11.2), using NixOS.

Possible links with pull request #1400

I have found this message on the GnuPG mailing list, which seems to show how to reproduce a similarly malformed request to the gpg passphrase prompt (and how to fix it). The error code is the same.

I'm trying to find the cause (see comments below) but I'm a total noob in Go, so I'm not sure I can fix this myself.

termaxima avatar Oct 17 '25 17:10 termaxima

Investigating a little more, it seems the offending call is here

req.CacheKey is somehow being set to an empty string (Since even a single-letter cacheID is valid)

termaxima avatar Oct 17 '25 18:10 termaxima

This function is then called only here, where location is likely already erroneously set to an empty string.

termaxima avatar Oct 17 '25 18:10 termaxima

location itself contains keys from the readers map, which are set by these variables :

  • SopsAgeKeyEnv
  • SopsAgeKeyFileEnv
  • SopsAgeKeyCmdEnv
  • ageKeyFilePath

One of these four is being set to an empty string somewhere, I'll try to find where.

(btw, probably not going to be able to fix this myself, as I have 0 skills in Go yet. I hope these comments will make the work easier tho)

termaxima avatar Oct 17 '25 18:10 termaxima

I think I have hit a dead end, I do not see how any of these variables can end up set to an empty string.

The most likely culprit is ageKeyFilePath, since the other ones are constants set at the top of the file to non-empty values, but I do not see a hole in the logic setting this variable.

userConfigDir, err := getUserConfigDir()

if err != nil && len(readers) == 0 && len(identities) == 0 {
    errs = append(errs, fmt.Errorf("user config directory could not be determined: %w", err))
} else if userConfigDir != "" {
    // This line is likely the culprit, but userConfigDir is guaranteed non-empty
    // by the very previous line, so the result of `filepath.Join` should at worst
    // be just `userConfigDir`
    ageKeyFilePath := filepath.Join(userConfigDir, filepath.FromSlash(SopsAgeKeyUserConfigPath))

    // But then this call would fail, and readers[ageKeyFilePath] would never get set.
    // and since ageKeyFilePath would be non-empty anyway, this is clearly not
    // what is happening
    f, err := os.Open(ageKeyFilePath)
    if err != nil && !errors.Is(err, os.ErrNotExist) {
        errs = append(errs, fmt.Errorf("failed to open file: %w", err))

    } else if errors.Is(err, os.ErrNotExist) && len(readers) == 0 && len(identities) == 0 {
        unusedLocations = append(unusedLocations, ageKeyFilePath)
    } else if err == nil {
        defer f.Close()
        readers[ageKeyFilePath] = f
    }
}

Higher in the file, here is where the other three variables are set :

const (
	// SopsAgeKeyEnv can be set as an environment variable with a string list
	// of age keys as value.
	SopsAgeKeyEnv = "SOPS_AGE_KEY" // NOTE: clearly non-empty
	// SopsAgeKeyFileEnv can be set as an environment variable pointing to an
	// age keys file.
	SopsAgeKeyFileEnv = "SOPS_AGE_KEY_FILE" // NOTE : clearly non-empty
	// SopsAgeKeyCmdEnv can be set as an environment variable with a command
	// to execute that returns the age keys.
	SopsAgeKeyCmdEnv = "SOPS_AGE_KEY_CMD" // NOTE : clealy non-empty
	// ...
)

termaxima avatar Oct 17 '25 18:10 termaxima

There is a possibility that the only other call to GetPassphrase is the offending one, as it is doing some weird stuff I don't understand. Link to the exact line

This would indicate a deeper problem, since I found this bug using an age key specifically, and the other call is supposedly only used for pgp keys...

termaxima avatar Oct 17 '25 18:10 termaxima

There is a possibility that the only other call to GetPassphrase is the offending one, as it is doing some weird stuff I don't understand. Link to the exact line

This would indicate a deeper problem, since I found this bug using an age key specifically, and the other call is supposedly only used for pgp keys...

After further investigation, this is impossible. The error messages I've shown are only produced with the correct call site for age keys.

termaxima avatar Oct 17 '25 18:10 termaxima

I'll let this be for now, I don't know how to use the language at all so I can't do any actual debugging in Go. I hope that by just reading the code, I've given some good leads to whoever ends up picking this issue up !

termaxima avatar Oct 17 '25 19:10 termaxima

If you look at https://dev.gnupg.org/source/gnupg/browse/master/agent/command.c$2082, you can see that the error also comes up if the length of location is > 50 bytes, and not only if it is empty. My guess is that the string happens to be longer than that.

(For some quick debugging, I usually insert fmt.Printf("%#v\n", valueOfInterest) and run make install, and then use ~/go/bin/sops.)

felixfontein avatar Oct 18 '25 08:10 felixfontein

If you look at https://dev.gnupg.org/source/gnupg/browse/master/agent/command.c$2082, you can see that the error also comes up if the length of location is > 50 bytes, and not only if it is empty. My guess is that the string happens to be longer than that.

(For some quick debugging, I usually insert fmt.Printf("%#v\n", valueOfInterest) and run make install, and then use ~/go/bin/sops.)

Indeed, seems more likely ! Though this means there are some shenanigans going on, as my key is stored under /home/termaxima/.config/sops/age/keys.txt which is only 41 characters. Maybe in the url stripping phase ?

Maybe the path should be hashed into a known correct size UUID instead ?

termaxima avatar Oct 20 '25 16:10 termaxima