You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
303 lines
11 KiB
303 lines
11 KiB
From: Daniel Kahn Gillmor <dkg@fifthhorseman.net>
|
|
Date: Tue, 11 Oct 2016 21:29:22 -0400
|
|
Subject: Test use of gpg without explicit passphrase (agent+pinentry)
|
|
|
|
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
|