Subject: Test use of gpg without explicit passphrase (agent+pinentry)
From: Daniel Kahn Gillmor <[email-masked]>
Date: Tue, 11 Oct 2016 21:29:22 -0400

The modern GnuPG suite encourages the use of gpg-agent to control
access to secret key material.  In this use case, we avoid setting an
explicit passphrase in code, and rely on either a correctly-configured
and primed gpg-agent or a dedicated pinentry program to supply the
passphrase.

This additional test verifies that the passphrase can be handled by
the agent.  Note that the passphrase for this additional test key is
*not* the default passphrase, so this test should fail in the event
that gpg can't use the agent and the pinentry for this task.

Unfortunately, this all assumes that we're using GnuPG "Modern".  I've
noted concerns about writing forward- and backward-compatible bindings
for GnuPG here:
https://lists.gnupg.org/pipermail/gnupg-devel/2016-October/031800.html
---
 README                 | 41 +++++++++++++++++++++++++++--------------
 lib/GnuPG/Interface.pm | 26 +++++++++++++++++++++++++-
 t/MyTestSpecific.pm    | 10 ++++++++--
 t/decrypt.t            | 27 +++++++++++++++++++++++++++
 test/encrypted.2.gpg   | 12 ++++++++++++
 test/fake-pinentry.pl  |  2 +-
 test/plain.2.txt       |  1 +
 7 files changed, 101 insertions(+), 18 deletions(-)
 create mode 100644 test/encrypted.2.gpg
 create mode 100644 test/plain.2.txt

diff --git a/README b/README
index a05ef9b..be06ef3 100644
--- a/README
+++ b/README
@@ -5,7 +5,7 @@ SYNOPSIS
       # A simple example
       use IO::Handle;
       use GnuPG::Interface;
-  
+
       # setting up the situation
       my $gnupg = GnuPG::Interface->new();
       $gnupg->options->hash_init( armor   => 1,
@@ -24,7 +24,7 @@ SYNOPSIS
       # Now we'll go about encrypting with the options already set
       my @plaintext = ( 'foobar' );
       my $pid = $gnupg->encrypt( handles => $handles );
-  
+
       # Now we write to the input of GnuPG
       print $input @plaintext;
       close $input;
@@ -140,13 +140,26 @@ OBJECT METHODS
         standard error, standard output, or standard error. If the status or
         logger handle is not defined, this channel of communication is never
         established with GnuPG, and so this information is not generated and
-        does not come into play. If the passphrase data member handle of the
-        handles object is not defined, but the the passphrase data member
-        handle of GnuPG::Interface object is, GnuPG::Interface will handle
-        passing this information into GnuPG for the user as a convenience.
-        Note that this will result in GnuPG::Interface storing the
-        passphrase in memory, instead of having it simply 'pass-through' to
-        GnuPG via a handle.
+        does not come into play.
+
+        If the passphrase data member handle of the handles object is not
+        defined, but the the passphrase data member handle of
+        GnuPG::Interface object is, GnuPG::Interface will handle passing
+        this information into GnuPG for the user as a convenience. Note that
+        this will result in GnuPG::Interface storing the passphrase in
+        memory, instead of having it simply 'pass-through' to GnuPG via a
+        handle.
+
+        If neither the passphrase data member of the GnuPG::Interface nor
+        the passphrase data member of the handles object is defined, then
+        GnuPG::Interface assumes that access and control over the secret key
+        will be handled by the running gpg-agent process. This represents
+        the simplest mode of operation with the GnuPG "modern" suite
+        (version 2.1 and later). It is also the preferred mode for tools
+        intended to be user-facing, since the user will be prompted directly
+        by gpg-agent for use of the secret key material. Note that for
+        programmatic use, this mode requires the gpg-agent and pinentry to
+        already be correctly configured.
 
   Other Methods
     get_public_keys( @search_strings )
@@ -241,7 +254,7 @@ EXAMPLES
 
       my $handles = GnuPG::Handles->new( stdin    => $input,
                                          stdout   => $output );
-   
+
       # this sets up the communication
       # Note that the recipients were specified earlier
       # in the 'options' data member of the $gnupg object.
@@ -315,7 +328,7 @@ EXAMPLES
       # a file written to disk
       # Make sure you "use IO::File" if you use this module!
       my $cipher_file = IO::File->new( 'encrypted.gpg' );
-   
+
       # this sets up the communication
       my $pid = $gnupg->decrypt( handles => $handles );
 
@@ -346,7 +359,7 @@ EXAMPLES
       # This time we'll just let GnuPG print to our own output
       # and read from our input, because no input is needed!
       my $handles = GnuPG::Handles->new();
-  
+
       my @ids = ( 'ftobin', '0xABCD1234ABCD1234ABCD1234ABCD1234ABCD1234' );
 
       # this time we need to specify something for
@@ -354,7 +367,7 @@ EXAMPLES
       # search ids as arguments
       my $pid = $gnupg->list_public_keys( handles      => $handles,
                                           command_args => [ @ids ] );
-  
+
        waitpid $pid, 0;
 
   Creating GnuPG::PublicKey Objects
@@ -372,7 +385,7 @@ EXAMPLES
           command_args => [ qw( test/key.1.asc ) ],
           handles      => $handles,
         );
-    
+
         my @out = <$handles->stdout()>;
         waitpid $pid, 0;
 
diff --git a/lib/GnuPG/Interface.pm b/lib/GnuPG/Interface.pm
index 29205f0..5d8b0ec 100644
--- a/lib/GnuPG/Interface.pm
+++ b/lib/GnuPG/Interface.pm
@@ -106,6 +106,14 @@ sub fork_attach_exec( $% ) {
     my ( $self, %args ) = @_;
 
     my $handles = $args{handles} or croak 'no GnuPG::Handles passed';
+    my $use_loopback_pinentry = 0;
+
+    # WARNING: this assumes that we're using the "modern" GnuPG suite
+    # -- version 2.1.x or later.  It's not clear to me how we can
+    # safely and efficiently avoid this assumption (see
+    # https://lists.gnupg.org/pipermail/gnupg-devel/2016-October/031800.html)
+    $use_loopback_pinentry = 1
+      if ($handles->passphrase());
 
     # deprecation support
     $args{commands} ||= $args{gnupg_commands};
@@ -293,8 +301,12 @@ sub fork_attach_exec( $% ) {
             $self->options->$option($fileno);
         }
 
+        my @args = $self->options->get_args();
+        push @args, '--pinentry-mode', 'loopback'
+          if $use_loopback_pinentry;
+
         my @command = (
-            $self->call(), $self->options->get_args(),
+            $self->call(), @args,
             @commands,     @command_args
         );
 
@@ -1005,6 +1017,7 @@ and standard error will be tied to the running program's standard error,
 standard output, or standard error.  If the B<status> or B<logger> handle
 is not defined, this channel of communication is never established with GnuPG,
 and so this information is not generated and does not come into play.
+
 If the B<passphrase> data member handle of the B<handles> object
 is not defined, but the the B<passphrase> data member handle of GnuPG::Interface
 object is, GnuPG::Interface will handle passing this information into GnuPG
@@ -1012,6 +1025,17 @@ for the user as a convenience.  Note that this will result in
 GnuPG::Interface storing the passphrase in memory, instead of having
 it simply 'pass-through' to GnuPG via a handle.
 
+If neither the B<passphrase> data member of the GnuPG::Interface nor
+the B<passphrase> data member of the B<handles> object is defined,
+then GnuPG::Interface assumes that access and control over the secret
+key will be handled by the running gpg-agent process.  This represents
+the simplest mode of operation with the GnuPG "modern" suite (version
+2.1 and later).  It is also the preferred mode for tools intended to
+be user-facing, since the user will be prompted directly by gpg-agent
+for use of the secret key material.  Note that for programmatic use,
+this mode requires the gpg-agent and pinentry to already be correctly
+configured.
+
 =back
 
 =head2 Other Methods
diff --git a/t/MyTestSpecific.pm b/t/MyTestSpecific.pm
index c8764cc..e513c25 100644
--- a/t/MyTestSpecific.pm
+++ b/t/MyTestSpecific.pm
@@ -55,9 +55,15 @@ struct( Text => { fn => "\$", fh => "\$", data => "\$" } );
 $texts{plain} = Text->new();
 $texts{plain}->fn( 'test/plain.1.txt' );
 
+$texts{alt_plain} = Text->new();
+$texts{alt_plain}->fn( 'test/plain.2.txt' );
+
 $texts{encrypted} = Text->new();
 $texts{encrypted}->fn( 'test/encrypted.1.gpg' );
 
+$texts{alt_encrypted} = Text->new();
+$texts{alt_encrypted}->fn( 'test/encrypted.2.gpg' );
+
 $texts{signed} = Text->new();
 $texts{signed}->fn( 'test/signed.1.asc' );
 
@@ -68,7 +74,7 @@ $texts{temp} = Text->new();
 $texts{temp}->fn( 'test/temp' );

-foreach my $name ( qw( plain encrypted signed key ) )
+foreach my $name ( qw( plain alt_plain encrypted alt_encrypted signed key ) )
 {
     my $entry = $texts{$name};
     my $filename = $entry->fn();
@@ -90,7 +96,7 @@ sub reset_handles
         stderr  => $stderr
       );
 
-    foreach my $name ( qw( plain encrypted signed key ) )
+    foreach my $name ( qw( plain alt_plain encrypted alt_encrypted signed key ) )
     {
         my $entry = $texts{$name};
         my $filename = $entry->fn();
diff --git a/t/decrypt.t b/t/decrypt.t
index b2639ed..ee41448 100644
--- a/t/decrypt.t
+++ b/t/decrypt.t
@@ -58,3 +58,30 @@ TEST
 {
     return compare( $texts{plain}->fn(), $texts{temp}->fn() ) == 0;
 };
+
+
+# test without default_passphrase (that is, by using the agent)
+TEST
+{
+    reset_handles();
+
+    $handles->stdin( $texts{alt_encrypted}->fh() );
+    $handles->options( 'stdin' )->{direct} = 1;
+
+    $handles->stdout( $texts{temp}->fh() );
+    $handles->options( 'stdout' )->{direct} = 1;
+
+    $gnupg->clear_passphrase();
+
+    my $pid = $gnupg->decrypt( handles => $handles );
+
+    waitpid $pid, 0;
+
+    return $CHILD_ERROR == 0;
+};
+
+
+TEST
+{
+    return compare( $texts{alt_plain}->fn(), $texts{temp}->fn() ) == 0;
+};
diff --git a/test/encrypted.2.gpg b/test/encrypted.2.gpg
new file mode 100644
index 0000000..105cbb3
--- /dev/null
+++ b/test/encrypted.2.gpg
@@ -0,0 +1,12 @@
+-----BEGIN PGP MESSAGE-----
+
+hQEMAw3NS2KuRB0PAQgAuCMQO6blPRIJZib+kDa51gac+BYPl8caXYTLqIHtiz2/
+YRVqePJON4lNAqT6qUksIzQHtejFO6tb1SLqgX9Ti+fKAMLrQw9VGOYaJFoRrTJs
++X33S4GHVVikRTu0dydAsekbfPSc2nRmTFUlSEV3psgAmg9xy8KA6cZroK9Xfcuh
+xW7KLE0hLP+2NZ7zNmJMdu6LDGzvlQsnm1UeElXK8XdMGf8kA3R+GgeeOnR/oEQc
+Uep77k/fLc+UV4fp9Dk1OBeg3Ko/irSaefk4mU7F4HmS8jIERHRvXBTiur1Zx8Nx
+9U3fcQuc+P9+JC89iS4PJPF1Hr0MlezAghZYJrhOrtJIAe5Uaft5KMGRfy0VQnAs
+MHqGnGtzzVWK6GK83ibgG4tTfPEHHIgNFsJf3rM4cWklUmCS9TeeDJJZfhnRA6+/
+X82e6OI7QNbO
+=DlGE
+-----END PGP MESSAGE-----
diff --git a/test/fake-pinentry.pl b/test/fake-pinentry.pl
index 12d3611..40b8b08 100755
--- a/test/fake-pinentry.pl
+++ b/test/fake-pinentry.pl
@@ -21,7 +21,7 @@ while (<STDIN>) {
   chomp;
   next if (/^$/);
   next if (/^#/);
-  print ("D test\n") if (/^getpin/i);
+  print ("D supercalifragilisticexpialidocious\n") if (/^getpin/i);
   print "OK\n";
   exit if (/^bye/i);
 }
diff --git a/test/plain.2.txt b/test/plain.2.txt
new file mode 100644
index 0000000..da5a1d5
--- /dev/null
+++ b/test/plain.2.txt
@@ -0,0 +1 @@
+test message