NicoHood / GPGit

A shell script that automates the process of signing Git sources via GPG
MIT License
89 stars 10 forks source link

For hwrng permission errors on Linux systems #35

Closed mzpqnxow closed 1 year ago

mzpqnxow commented 2 years ago

Hello, I recently stumbled across this project while writing something similar for colleagues to use in a GitHub Enterprise instance. As I was testing it myself (on a Debian Linux system) I noticed that generation of the gpg keypair was failing with EPERM. I figured it was probably due to the hardware RNG device on my system and was correct

In case anyone encounters this, it can be fixed with a udev rule, placed in /etc/udev/rules.d/30-hwrng.rule as follows:

KERNEL=="hw_random",NAME="hwrng", SYMLINK+="%k" MODE="0666"

After this, run sudo udevadm trigger to cause the permissiosn to be set. It should persist across reboots. I believe the writable permissions are safe as writes to /dev/hwrng (if implemented) only "stir" the entropy and can not cause it to be weakened in any way. Paranoid users could use 644, or 660 and a trusted group as the gid

As an alternative and inferior solution (because it won't persist across reboots) one can also use chmod 666 /dev/hwrng (though this won't persist across reboots)

I'm not sure if it's the job of GPGit to ensure that (if present) a hw rng device has suitable permissions- and I'm actually surprised that gpg doesn't fall back to /dev/random or /dev/urandom when it gets EPERM on /dev/hwrng- so I don't expect you to incorporate any changes into GPGit. This issue is just here for users that encounter the issue that may not be too familiar with how gpg (or hardware rngs) work

The error you will see in this case is:

    Generating the new GPG key with the selected parameters now.
Continue? [Y/n]y
We need to generate a lot of random bytes. It is a good idea to perform
some other action (type on the keyboard, move the mouse, utilize the
disks) during the prime generation; this gives the random number
generator a better chance to gain enough entropy.
Key generation failed: Permission denied

Please feel free to close this out once you've seen it. If you decide that you do want to make a change, I assume it would only be in the documentation, or maybe in the error message- if so, I'm happy to send a small PR for that, just let me know

Thanks!

mzpqnxow commented 2 years ago

Also, while we're on the subject- I encountered another permission error that was fixed with the addition of --pinentry-mode loopback to the gpg2 key generation command

I think this may have to do with having used sudo to the account in which I was using GPGit. This occurred after the hwrng permissions issue was fixed. The error is as follows (generic, I traced it down to pin entry with strace and saw the PINENTRY command going

gpg: agent_genkey failed: Permission denied
Key generation failed: Permission denied

The gpg2 process is using the default GPG agent socket, which on Debian is /run/user/<uid>/gnupg/S.gpg-agent. I checked that the permissions on the socket were correct (owned by this user, mode 600) and confirmed that the socket was successfully opened

NicoHood commented 2 years ago

Thanks for reporting!

Is there a special return code, that we could check for? If yes, I could output a more detailed error message.

The use of the hardware rng is documented somewhere? I had a quick look at my code, but did I set that in the gpgit code?

About your seconds comment: So the issue is, that gpgit cannot run with root privileges? Maybe we should add a general waring when run with root? What does the pinentry mode really do, it is still not clear to me.

mzpqnxow commented 2 years ago

Thanks for reporting!

Thanks for your work on this project, I've been slowly piecing together the same functionality as I've needed it and am very happy to throw what I have away in favor of yours :)

Is there a special return code, that we could check for? If yes, I could output a more detailed error message.

I assume gpg2 will emit a distinct exit code for EPERM-based failures but I haven't confirmed that.

I'm hopeful there's a simple way to workaround these issues entirely, though, so it won't be necessary to look at the exit code at all

The use of the hardware rng is documented somewhere? I had a quick look at my code, but did I set that in the gpgit code?

This isn't your issue, though you may have to do something to handle configurations like mine (hardware RNG, use of gpg-agent, Debian and Ubuntu GPG agent configurations)

I'm sure I can find a clean and non-invasive way around this. It's confusing to me that gpg2/gpg-agent aren't using /dev/random by default, and it's even more confusing to me as to why both Debian and Ubuntu set such restrictive permissions on /dev/hwrng by default. I would expect the driver to create it with the same permissions as urandom and random

So the issue is, that gpgit cannot run with root privileges? Maybe we should add a general waring when run with root

Maybe I misspoke, to clarify, I was not running with root privileges. I used ssh to user1, followed by sudo to user2. That's where I encountered the issue.

What does the pinentry mode really do, it is still not clear to me.

If I have it correct, if gpg-agent is present, gpg2 sends messages (see --list-packets with gpg2) to the gpg-agent process via a UNIX socket to perform the various operations- one of which is PINENTRY

At that point, gpg-agent performs the necessary steps to cause a prompt for the user, for a SC/TPM PIN or passphrase. Users with functional X11 will usually get an X11-based prompt but there's also an ncurses-based pin entry app, which performs some ioctl() operations on the raw TTY character device

I think because I used sudo from one user to another, and had no X11 display, the TTY pin entry mode was attempted but failed with a permission error because the original user (before sudo) maintains ownership of the TTY

When specifying loopback mode, gpg2 uses a very primitive (stdin) prompt, as opposed to delegating raw operations on the TTY to gpg-agent. They refer to it as loopback presumably to describe the fact that it doesn't delegate the pinentry to a pinentry application like it usually would

It's worth mentioning that delegating the pin entry in this way was a very deliberate design decision made for security reasons- more on this here

Anyway, to reinforce that this issue is due to my environment and not the fault of GPGit, I can share that I've had similar issues with other applications that require low-level interaction with the TTY device. For example, after using the sudo from user1 to user2 process, GNU screen fails due to permission errors.

A workaround (for screen, and any other application that needs raw TTY access) is to cause allocation of a full TTY after the sudo. To accomplish this, you have a few options. You can use something like 'script /dev/null' or 'ssh localhost' which both ultimately provide a new TTY fully owned by the user. It may be that you'll need to decide if the security risk associated with loopback is problematic enough to simply not support use of GPGit in this manner

Fibally, I'm on mobile so this will probably be formatted incorrectly, but here's the documentation on the different modes:

'''

      default
             Use the default of the agent, which is ask.

      ask    Force the use of the Pinentry.

      cancel Emulate use of Pinentry's cancel button.

      error  Return a Pinentry error (``No Pinentry'').

      loopback
             Redirect Pinentry queries to the caller.  Note that in contrast to Pinentry the user is not prompted again if he enters a bad password.

'''

tl;dr; The issues are specific to my environment so I'll get back to you with proposals for "fixes" and any associated caveats (for both issues)

Thanks again

(sorry for formatting/redundancy, I'm on mobile)

NicoHood commented 2 years ago

Alright, yes I think this should be best used in your environment. Nevertheless it would be nice, if you can check that exit code, so we can show the user a special note to this issue, so he can read up on it more, as you've done a very good reasearch so far.

mzpqnxow commented 2 years ago

FYI, using the following:

diff --git a/gpgit.sh b/gpgit.sh
index 00004c2..a51332e 100755
--- a/gpgit.sh
+++ b/gpgit.sh
@@ -243,9 +243,9 @@ function parse_keepachangelog()
 }

 # Trap errors
-set -o errexit -o errtrace -u
-trap 'die "Error on or near line ${LINENO}. Please report this issue: https://github.com/NicoHood/gpgit/issues"' ERR
-trap kill_exit SIGTERM SIGINT SIGHUP
+# set -o errexit -o errtrace -u
+# trap 'die "Error on or near line ${LINENO}. Please report this issue: https://github.com/NicoHood/gpgit/issues"' ERR
+# trap kill_exit SIGTERM SIGINT SIGHUP

 # Initialize variables
 unset INTERACTIVE MESSAGE KEYSERVER COMPRESSION HASH OUTPUT PROJECT SIGNINGKEY
@@ -586,8 +586,11 @@ if [[ -z "${SIGNINGKEY}" ]]; then
     # Generate strongest possible GPG key (ECC or RSA4096, depending on gnupg version)
     plain "Generating the new GPG key with the selected parameters now."
     interactive
-    ${GPG_BIN} --quick-generate-key "${GPG_USER} <${GPG_EMAIL}>" future-default default 1y \
-        &> /dev/null || die "GPG key generation aborted."
+    ${GPG_BIN} --quick-generate-key "${GPG_USER} <${GPG_EMAIL}>" future-default default 1y
+    echo $?
+    exit 0
+#    ${GPG_BIN} --quick-generate-key "${GPG_USER} <${GPG_EMAIL}>" future-default default 1y \
+#        &> /dev/null || die "GPG key generation aborted."
     SIGNINGKEY="$(${GPG_BIN} --with-colons --list-secret-keys | grep -F -B 2 "${GPG_USER} <${GPG_EMAIL}>" | awk -F: '$1 == "fpr" {print $10;}')"
     NEW_SIGNINGKEY="true"
     plain "Your new GPG fingerprint is: '${SIGNINGKEY}'"

The exit code is 2

This is what I see:

gpgit@host:~/GPGit$ ./gpgit.sh master
GPGit 1.5.0 https://github.com/NicoHood/gpgit

Running GPGit for the first time. This will guide you through all steps of secure source code signing once. If you wish to run interactively again pass the -i option to GPGit. For more options see --help.
Continue? [Y/n]y
    A Github repository was detected, but no token was provided.
    You can disable Github release uploading with:
    git config gpgit.github false
    Please enter your Github token or generate a new one (permission: 'public_repo'):
    https://github.com/settings/tokens
    Tip: Configure your Github token permanant with:
    git config --global gpgit.token <token>
==> 1. Generate a new GPG key
  -> 1.1 Strong, unique, secret passphrase
    See: https://github.com/NicoHood/gpgit#11-strong-unique-secret-passphrase
  -> 1.2 Key generation
    No GPG key registered with Git. Generating a new GPG key.
gpg: directory '/home/gpgit/.gnupg' created
gpg: keybox '/home/gpgit/.gnupg/pubring.kbx' created
gpg: /home/gpgit/.gnupg/trustdb.gpg: trustdb created
Enter username: gpgit
Enter email: test@test.com

    Generating the new GPG key with the selected parameters now.
Continue? [Y/n]y
We need to generate a lot of random bytes. It is a good idea to perform
some other action (type on the keyboard, move the mouse, utilize the
disks) during the prime generation; this gives the random number
generator a better chance to gain enough entropy.
gpg: agent_genkey failed: Permission denied
Key generation failed: Permission denied
2

So you could link to this issue, or here, or one of the other links above upon failure- but you would need to say "if you see permission denied, see this link", as it seems 2 is used quite commonly for fatal errors with gpg2, e.g. the following also exits with 2:

gpgit@host:~/GPGit$ gpg2 --pinentry-mode loopback -vvvvv  --quick-generate-key 'gpgit <test@test.com>' future-default default INVALID_TIME
gpg: using character set 'utf-8'
gpg: Key generation failed: Invalid value
gpgit@host:~/GPGit$ echo $?
2

I'll leave it up to you to determine what (if anything) you would like to do here (if anything)

It's worth mentioning that all gpg2 operations requiring passphrase entry will fail in an environment like this- including signing and decrypting. Which I think highlights that this is not an issue with GPGit

So, the workarounds:

cat >> ~/.gnupg/gpg.conf << 'EOF'
pinentry-mode loopback 
EOF

Or, add --pinentry-mode loopback to the CLI. However, I don't think this should be the default behavior of GPGit. Deferring to the environment as-is is the correct thing, personally I would be very annoyed if GPGit forced me to loopback when I had a customized pinentry mechanism configured ;)

EDIT: To clarify how to reproduce this: sudo su - otheruser; ./gpgit.sh on (probably) any Linux distribution, but definitely Debian / Ubuntu derived distributions