This is an automated email from the ASF dual-hosted git repository.

mattcasters pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hop.git


The following commit(s) were added to refs/heads/main by this push:
     new 82cefaff3d Fixes #8659 : Choose the PGP signing key per row (#8661)
82cefaff3d is described below

commit 82cefaff3d499b9d5009f1eefde6057176d95e29
Author: Bart Maertens <[email protected]>
AuthorDate: Thu Oct 1 20:40:54 2026 +0200

    Fixes #8659 : Choose the PGP signing key per row (#8661)
    
    * Issue #8659 : Choose the PGP signing key per row
    
    Add a Signing key column to the PGP encrypt files action, passed to GnuPG 
as -u, so Sign and Sign and Encrypt rows can name the key that signs instead of 
taking whatever default-key gpg.conf holds.
    
    Sign rows passed the User ID as -r, which GnuPG accepts and ignores for 
anything but encryption, so the key named there never had any effect. It is now 
left unused and logged, rather than promoted to the signing key: an existing 
workflow keeps signing with the same key it always did.
    
    Covered by unit tests on the argument list, end-to-end tests against a real 
gpg with two keys in the keyring, and integration test main-0009, which asserts 
both the new choice and the unchanged behaviour of rows that name no signing 
key.
    
    * Issue #8659 : Address review comments
---
 .../pages/workflow/actions/pgpencryptfiles.adoc    |   5 +-
 .../pgp/main-0009-pgp-choose-the-signing-key.hwf   | 446 +++++++++++++++++++++
 .../pgpencryptfiles/ActionPGPEncryptFiles.java     |  49 ++-
 .../ActionPGPEncryptFilesDialog.java               |  16 +-
 .../hop/workflow/actions/pgpencryptfiles/GPG.java  | 121 +++++-
 .../messages/messages_en_US.properties             |   7 +-
 .../ActionPGPEncryptFilesDialogTest.java           |  16 +-
 .../ActionPGPEncryptFilesSignTest.java             | 167 +++++++-
 .../pgpencryptfiles/ActionPGPEncryptFilesTest.java |   5 +
 .../pgpencryptfiles/GpgArgumentPassingTest.java    |  74 +++-
 .../test/resources/action-pgp-encrypt-files.xml    |   2 +
 11 files changed, 857 insertions(+), 51 deletions(-)

diff --git 
a/docs/hop-user-manual/modules/ROOT/pages/workflow/actions/pgpencryptfiles.adoc 
b/docs/hop-user-manual/modules/ROOT/pages/workflow/actions/pgpencryptfiles.adoc
index 0d6ac8479b..876e710fa6 100644
--- 
a/docs/hop-user-manual/modules/ROOT/pages/workflow/actions/pgpencryptfiles.adoc
+++ 
b/docs/hop-user-manual/modules/ROOT/pages/workflow/actions/pgpencryptfiles.adoc
@@ -37,12 +37,13 @@ GnuPG package must be installed in the runtime environment 
and encryption keys a
 |GPG location|The file path of the GnuPG executable (e.g. `/usr/bin/gpg`).
 |Use ASCII mode|Whether or not to use ASCII mode when encrypting.
 |Include subfolders|Whether or not to include subfolders.
-|Copy previous results to args|
+|Copy previous results to args|Take the rows below from the result of the 
previous action instead of from the grid. The fields are read by position: 
action, file/folder source, wildcard, user ID, file/folder destination, and 
optionally signing key as a sixth field.
 |Action|Encrypt, Sign, Sign and Encrypt
 |File/Folder source|The file to encrypt, can be added to the Files/Folders 
list using Add.
 |File/Folder destination|The destination of the encrypted files.
 |Wildcard|A regex wildcard.
-// |User ID|User ID of the key in the runtime user keystore
+|User ID|User ID of the key to encrypt to, passed to GnuPG as `-r`. Required 
for Encrypt and Sign and Encrypt, and not used by Sign.
+|Signing key|User ID of the key to sign with, passed to GnuPG as `-u`. Used by 
Sign and Sign and Encrypt; leave it empty to sign with the default key from the 
GnuPG configuration.
 |Files/Folders|The files to encrypt.
 |===
 
diff --git a/integration-tests/pgp/main-0009-pgp-choose-the-signing-key.hwf 
b/integration-tests/pgp/main-0009-pgp-choose-the-signing-key.hwf
new file mode 100644
index 0000000000..913e4f8444
--- /dev/null
+++ b/integration-tests/pgp/main-0009-pgp-choose-the-signing-key.hwf
@@ -0,0 +1,446 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
+-->
+<workflow>
+  <name>main-0009-pgp-choose-the-signing-key</name>
+  <name_sync_with_filename>Y</name_sync_with_filename>
+  <description>Signing key per row (gpg -u), and the backwards compatibility 
of rows that have none. See 
https://github.com/apache/hop/issues/8659</description>
+  <extended_description/>
+  <workflow_version/>
+  <created_user>-</created_user>
+  <created_date>2026/09/01 00:00:00.000</created_date>
+  <modified_user>-</modified_user>
+  <modified_date>2026/09/01 00:00:00.000</modified_date>
+  <parameters>
+    </parameters>
+  <actions>
+    <action>
+      <name>Start</name>
+      <description/>
+      <type>SPECIAL</type>
+      <attributes/>
+      <DayOfMonth>1</DayOfMonth>
+      <doNotWaitOnFirstExecution>N</doNotWaitOnFirstExecution>
+      <hour>12</hour>
+      <intervalMinutes>60</intervalMinutes>
+      <intervalSeconds>0</intervalSeconds>
+      <minutes>0</minutes>
+      <repeat>N</repeat>
+      <schedulerType>0</schedulerType>
+      <weekDay>1</weekDay>
+      <parallel>N</parallel>
+      <xloc>80</xloc>
+      <yloc>80</yloc>
+      <attributes_hac/>
+    </action>
+    <action>
+      <name>create a keyring with two keys</name>
+      <description/>
+      <type>SHELL</type>
+      <attributes/>
+      <filename/>
+      <work_directory>${PROJECT_HOME}</work_directory>
+      <arg_from_previous>N</arg_from_previous>
+      <exec_per_row>N</exec_per_row>
+      <set_logfile>N</set_logfile>
+      <logfile/>
+      <set_append_logfile>N</set_append_logfile>
+      <logext/>
+      <add_date>N</add_date>
+      <add_time>N</add_time>
+      <insertScript>Y</insertScript>
+      <script>#!/bin/bash
+set -e
+
+# Two usable secret keys, so "which key signed this" has more than one answer 
and gpg's own
+# default can only be one of them. A throwaway keyring under output/, so a 
test run never
+# touches the keyring of the machine or container it happens to run on.
+GNUPG_HOME=${PROJECT_HOME}/output/gnupg-two-keys
+WRAPPER=${PROJECT_HOME}/output/gpg-two-keys.sh
+
+GPG_BIN=$(command -v gpg || true)
+if [ -z "$GPG_BIN" ]; then
+  echo "gpg is not installed; the PGP actions cannot run without it"
+  exit 1
+fi
+
+# gpg starts an agent bound to this home's socket and leaves it running. 
Deleting the home
+# under a live agent, then racing gpg's five-second auto-launch of a fresh 
one, makes key
+# generation fail now and then with "No agent running" or "Broken pipe". Stop 
the previous
+# test's agent first.
+if [ -d "$GNUPG_HOME" ]; then
+  gpgconf --homedir "$GNUPG_HOME" --kill all &gt;/dev/null 2&gt;&amp;1 || true
+fi
+rm -rf "$GNUPG_HOME"
+mkdir -p "$GNUPG_HOME"
+chmod 700 "$GNUPG_HOME"
+
+# gpg is started by the Hop JVM and inherits its environment, and a Java 
process cannot change
+# its own, so GNUPGHOME can never reach it from here. Pin the keyring in a 
wrapper instead and
+# point the PGP actions at that.
+printf '#!/bin/sh\nexec "%s" --homedir "%s" "$@"\n' "$GPG_BIN" "$GNUPG_HOME" 
&gt; "$WRAPPER"
+chmod 700 "$WRAPPER"
+
+# Start the agent for this home ourselves, and wait for it, before asking it 
for keys.
+gpg-connect-agent --homedir "$GNUPG_HOME" /bye &gt;/dev/null 2&gt;&amp;1 || 
true
+
+# The keys carry no passphrase: the PGP encrypt action has no passphrase 
field, so signing can
+# only ever use a key gpg is able to unlock on its own.
+for USER_ID in "Hop IT Default &lt;[email protected]&gt;" "Hop IT 
Signer &lt;[email protected]&gt;"; do
+  "$GPG_BIN" --homedir "$GNUPG_HOME" --batch --yes --pinentry-mode loopback 
--passphrase '' \
+    --quiet --quick-generate-key "$USER_ID" default default never 2&gt;&amp;1
+done
+
+printf 'id,amount\n1,100\n2,200\n' &gt; 
${PROJECT_HOME}/output/signing-key-source.csv
+rm -f ${PROJECT_HOME}/output/signed-by-chosen-key.asc \
+      ${PROJECT_HOME}/output/signed-with-user-id-only.asc \
+      ${PROJECT_HOME}/output/sealed-and-signed.asc
+</script>
+      <loglevel>Basic</loglevel>
+      <parallel>N</parallel>
+      <xloc>240</xloc>
+      <yloc>80</yloc>
+      <attributes_hac/>
+    </action>
+    <action>
+      <name>sign with the chosen key</name>
+      <description>Signing key filled in: gpg must sign with that key and not 
with its default.</description>
+      <type>PGP_ENCRYPT_FILES</type>
+      <attributes/>
+      <gpglocation>${PROJECT_HOME}/output/gpg-two-keys.sh</gpglocation>
+      <arg_from_previous>N</arg_from_previous>
+      <include_subfolders>N</include_subfolders>
+      <add_result_filesname>N</add_result_filesname>
+      <destination_is_a_file>Y</destination_is_a_file>
+      <create_destination_folder>N</create_destination_folder>
+      <add_date>N</add_date>
+      <add_time>N</add_time>
+      <SpecifyFormat>N</SpecifyFormat>
+      <date_time_format/>
+      <nr_errors_less_than>10</nr_errors_less_than>
+      <success_condition>success_if_no_errors</success_condition>
+      <AddDateBeforeExtension>N</AddDateBeforeExtension>
+      <DoNotKeepFolderStructure>N</DoNotKeepFolderStructure>
+      <iffileexists>overwrite_file</iffileexists>
+      <destinationFolder/>
+      <ifmovedfileexists>do_nothing</ifmovedfileexists>
+      <moved_date_time_format/>
+      <create_move_to_folder>N</create_move_to_folder>
+      <add_moved_date>N</add_moved_date>
+      <add_moved_time>N</add_moved_time>
+      <SpecifyMoveFormat>N</SpecifyMoveFormat>
+      <AddMovedDateBeforeExtension>N</AddMovedDateBeforeExtension>
+      <asciiMode>Y</asciiMode>
+      <fields>
+        <field>
+          <action_type>sign</action_type>
+          
<source_filefolder>${PROJECT_HOME}/output/signing-key-source.csv</source_filefolder>
+          <wildcard/>
+          <userid></userid>
+          <local_user>[email protected]</local_user>
+          
<destination_filefolder>${PROJECT_HOME}/output/signed-by-chosen-key.asc</destination_filefolder>
+        </field>
+      </fields>
+      <parallel>N</parallel>
+      <xloc>460</xloc>
+      <yloc>80</yloc>
+      <attributes_hac/>
+    </action>
+    <action>
+      <name>sign with a user id and no signing key</name>
+      <description>A row as written before the signing key existed. The User 
ID is not the key that signs, and never was: it went to gpg as -r, which it 
ignores outside encryption. Such a row has to keep signing with the default 
key.</description>
+      <type>PGP_ENCRYPT_FILES</type>
+      <attributes/>
+      <gpglocation>${PROJECT_HOME}/output/gpg-two-keys.sh</gpglocation>
+      <arg_from_previous>N</arg_from_previous>
+      <include_subfolders>N</include_subfolders>
+      <add_result_filesname>N</add_result_filesname>
+      <destination_is_a_file>Y</destination_is_a_file>
+      <create_destination_folder>N</create_destination_folder>
+      <add_date>N</add_date>
+      <add_time>N</add_time>
+      <SpecifyFormat>N</SpecifyFormat>
+      <date_time_format/>
+      <nr_errors_less_than>10</nr_errors_less_than>
+      <success_condition>success_if_no_errors</success_condition>
+      <AddDateBeforeExtension>N</AddDateBeforeExtension>
+      <DoNotKeepFolderStructure>N</DoNotKeepFolderStructure>
+      <iffileexists>overwrite_file</iffileexists>
+      <destinationFolder/>
+      <ifmovedfileexists>do_nothing</ifmovedfileexists>
+      <moved_date_time_format/>
+      <create_move_to_folder>N</create_move_to_folder>
+      <add_moved_date>N</add_moved_date>
+      <add_moved_time>N</add_moved_time>
+      <SpecifyMoveFormat>N</SpecifyMoveFormat>
+      <AddMovedDateBeforeExtension>N</AddMovedDateBeforeExtension>
+      <asciiMode>Y</asciiMode>
+      <fields>
+        <field>
+          <action_type>sign</action_type>
+          
<source_filefolder>${PROJECT_HOME}/output/signing-key-source.csv</source_filefolder>
+          <wildcard/>
+          <userid>[email protected]</userid>
+          
<destination_filefolder>${PROJECT_HOME}/output/signed-with-user-id-only.asc</destination_filefolder>
+        </field>
+      </fields>
+      <parallel>N</parallel>
+      <xloc>680</xloc>
+      <yloc>80</yloc>
+      <attributes_hac/>
+    </action>
+    <action>
+      <name>seal to one key and sign with another</name>
+      <description>Recipient and signer are different keys, which is what 
having both options is for.</description>
+      <type>PGP_ENCRYPT_FILES</type>
+      <attributes/>
+      <gpglocation>${PROJECT_HOME}/output/gpg-two-keys.sh</gpglocation>
+      <arg_from_previous>N</arg_from_previous>
+      <include_subfolders>N</include_subfolders>
+      <add_result_filesname>N</add_result_filesname>
+      <destination_is_a_file>Y</destination_is_a_file>
+      <create_destination_folder>N</create_destination_folder>
+      <add_date>N</add_date>
+      <add_time>N</add_time>
+      <SpecifyFormat>N</SpecifyFormat>
+      <date_time_format/>
+      <nr_errors_less_than>10</nr_errors_less_than>
+      <success_condition>success_if_no_errors</success_condition>
+      <AddDateBeforeExtension>N</AddDateBeforeExtension>
+      <DoNotKeepFolderStructure>N</DoNotKeepFolderStructure>
+      <iffileexists>overwrite_file</iffileexists>
+      <destinationFolder/>
+      <ifmovedfileexists>do_nothing</ifmovedfileexists>
+      <moved_date_time_format/>
+      <create_move_to_folder>N</create_move_to_folder>
+      <add_moved_date>N</add_moved_date>
+      <add_moved_time>N</add_moved_time>
+      <SpecifyMoveFormat>N</SpecifyMoveFormat>
+      <AddMovedDateBeforeExtension>N</AddMovedDateBeforeExtension>
+      <asciiMode>Y</asciiMode>
+      <fields>
+        <field>
+          <action_type>signandencrypt</action_type>
+          
<source_filefolder>${PROJECT_HOME}/output/signing-key-source.csv</source_filefolder>
+          <wildcard/>
+          <userid>[email protected]</userid>
+          <local_user>[email protected]</local_user>
+          
<destination_filefolder>${PROJECT_HOME}/output/sealed-and-signed.asc</destination_filefolder>
+        </field>
+      </fields>
+      <parallel>N</parallel>
+      <xloc>900</xloc>
+      <yloc>80</yloc>
+      <attributes_hac/>
+    </action>
+    <action>
+      <name>check which key signed each file</name>
+      <description/>
+      <type>SHELL</type>
+      <attributes/>
+      <filename/>
+      <work_directory>${PROJECT_HOME}</work_directory>
+      <arg_from_previous>N</arg_from_previous>
+      <exec_per_row>N</exec_per_row>
+      <set_logfile>N</set_logfile>
+      <logfile/>
+      <set_append_logfile>N</set_append_logfile>
+      <logext/>
+      <add_date>N</add_date>
+      <add_time>N</add_time>
+      <insertScript>Y</insertScript>
+      <script>#!/bin/bash
+
+GPG="${PROJECT_HOME}/output/gpg-two-keys.sh"
[email protected]
[email protected]
+
+# Which key does gpg reach for when nothing names one? Asked rather than 
assumed, so the
+# backwards compatibility check below does not depend on the order the keys 
were generated in.
+printf 'probe\n' &gt; ${PROJECT_HOME}/output/probe.txt
+"$GPG" --batch --yes --armor --output ${PROJECT_HOME}/output/probe.asc 
--clearsign \
+  -- ${PROJECT_HOME}/output/probe.txt
+if "$GPG" --batch --verify ${PROJECT_HOME}/output/probe.asc 2&gt;&amp;1 | grep 
-q "$SIGNER_KEY"; then
+  GPG_DEFAULT="$SIGNER_KEY"
+else
+  GPG_DEFAULT="$DEFAULT_KEY"
+fi
+echo "gpg signs with $GPG_DEFAULT when no key is named"
+
+# Every check runs, so one run reports everything that is wrong rather than 
only the first
+# thing. FAILURES is what the action's exit code is built from at the end.
+FAILURES=0
+
+fail() {
+  echo "FAIL: $*"
+  FAILURES=$((FAILURES + 1))
+}
+
+signature_report() {
+  "$GPG" --batch --verify "$1" 2&gt;&amp;1 || true
+}
+
+assert_signed_by() {
+  local file="$1" expected="$2" what="$3" report
+  report=$(signature_report "$file")
+  if echo "$report" | grep -q "$expected"; then
+    echo "OK: $what is signed by $expected"
+  else
+    fail "$what: expected a signature from $expected"
+    echo "$report"
+  fi
+}
+
+# The feature: the row named a signing key, so that key has to be the one that 
signed.
+assert_signed_by ${PROJECT_HOME}/output/signed-by-chosen-key.asc "$SIGNER_KEY" 
\
+  "a Sign row with a signing key"
+
+# Backwards compatibility: a row with only a User ID has always signed with 
the default key,
+# and still has to. Only meaningful while the two keys differ.
+if [ "$GPG_DEFAULT" = "$SIGNER_KEY" ]; then
+  fail "gpg's default key is the signer, so the backwards compatibility check 
proves nothing"
+else
+  assert_signed_by ${PROJECT_HOME}/output/signed-with-user-id-only.asc 
"$GPG_DEFAULT" \
+    "a Sign row with only a User ID"
+  if signature_report ${PROJECT_HOME}/output/signed-with-user-id-only.asc | 
grep -q "$SIGNER_KEY"; then
+    fail "a User ID on its own must not become the signing key"
+  else
+    echo "OK: a User ID on its own did not become the signing key"
+  fi
+fi
+
+# Sealed to one key, signed with another: decrypting reports both halves in 
one go.
+REPORT=$("$GPG" --batch --decrypt ${PROJECT_HOME}/output/sealed-and-signed.asc 
2&gt;&amp;1 || true)
+if echo "$REPORT" | grep -q "$SIGNER_KEY"; then
+  echo "OK: sealed to $DEFAULT_KEY and signed by $SIGNER_KEY"
+else
+  fail "the sealed file must carry a signature from $SIGNER_KEY"
+  echo "$REPORT"
+fi
+if echo "$REPORT" | grep -q '1,100'; then
+  echo "OK: the sealed file decrypts to the original content"
+else
+  fail "the sealed file must decrypt to the original content"
+  echo "$REPORT"
+fi
+
+gpgconf --homedir ${PROJECT_HOME}/output/gnupg-two-keys --kill all 
&gt;/dev/null 2&gt;&amp;1 || true
+
+if [ "$FAILURES" -ne 0 ]; then
+  echo "$FAILURES check(s) failed"
+  exit 1
+fi
+echo "all checks passed"
+</script>
+      <loglevel>Basic</loglevel>
+      <parallel>N</parallel>
+      <xloc>1120</xloc>
+      <yloc>80</yloc>
+      <attributes_hac/>
+    </action>
+    <action>
+      <name>Abort workflow</name>
+      <description/>
+      <type>ABORT</type>
+      <attributes/>
+      <message>Choosing the PGP signing key did not behave as 
expected</message>
+      <loglevel>ERROR</loglevel>
+      <always_log_rows>N</always_log_rows>
+      <parallel>N</parallel>
+      <xloc>680</xloc>
+      <yloc>220</yloc>
+      <attributes_hac/>
+    </action>
+  </actions>
+  <hops>
+    <hop>
+      <from>Start</from>
+      <to>create a keyring with two keys</to>
+      <enabled>Y</enabled>
+      <evaluation>Y</evaluation>
+      <unconditional>Y</unconditional>
+    </hop>
+    <hop>
+      <from>create a keyring with two keys</from>
+      <to>sign with the chosen key</to>
+      <enabled>Y</enabled>
+      <evaluation>Y</evaluation>
+      <unconditional>N</unconditional>
+    </hop>
+    <hop>
+      <from>sign with the chosen key</from>
+      <to>sign with a user id and no signing key</to>
+      <enabled>Y</enabled>
+      <evaluation>Y</evaluation>
+      <unconditional>N</unconditional>
+    </hop>
+    <hop>
+      <from>sign with a user id and no signing key</from>
+      <to>seal to one key and sign with another</to>
+      <enabled>Y</enabled>
+      <evaluation>Y</evaluation>
+      <unconditional>N</unconditional>
+    </hop>
+    <hop>
+      <from>seal to one key and sign with another</from>
+      <to>check which key signed each file</to>
+      <enabled>Y</enabled>
+      <evaluation>Y</evaluation>
+      <unconditional>N</unconditional>
+    </hop>
+    <hop>
+      <from>create a keyring with two keys</from>
+      <to>Abort workflow</to>
+      <enabled>Y</enabled>
+      <evaluation>N</evaluation>
+      <unconditional>N</unconditional>
+    </hop>
+    <hop>
+      <from>sign with the chosen key</from>
+      <to>Abort workflow</to>
+      <enabled>Y</enabled>
+      <evaluation>N</evaluation>
+      <unconditional>N</unconditional>
+    </hop>
+    <hop>
+      <from>sign with a user id and no signing key</from>
+      <to>Abort workflow</to>
+      <enabled>Y</enabled>
+      <evaluation>N</evaluation>
+      <unconditional>N</unconditional>
+    </hop>
+    <hop>
+      <from>seal to one key and sign with another</from>
+      <to>Abort workflow</to>
+      <enabled>Y</enabled>
+      <evaluation>N</evaluation>
+      <unconditional>N</unconditional>
+    </hop>
+    <hop>
+      <from>check which key signed each file</from>
+      <to>Abort workflow</to>
+      <enabled>Y</enabled>
+      <evaluation>N</evaluation>
+      <unconditional>N</unconditional>
+    </hop>
+  </hops>
+  <notepads>
+  </notepads>
+  <attributes/>
+</workflow>
diff --git 
a/plugins/actions/pgpfiles/src/main/java/org/apache/hop/workflow/actions/pgpencryptfiles/ActionPGPEncryptFiles.java
 
b/plugins/actions/pgpfiles/src/main/java/org/apache/hop/workflow/actions/pgpencryptfiles/ActionPGPEncryptFiles.java
index 3fa261c892..9e4340e8ef 100644
--- 
a/plugins/actions/pgpfiles/src/main/java/org/apache/hop/workflow/actions/pgpencryptfiles/ActionPGPEncryptFiles.java
+++ 
b/plugins/actions/pgpfiles/src/main/java/org/apache/hop/workflow/actions/pgpencryptfiles/ActionPGPEncryptFiles.java
@@ -330,6 +330,7 @@ public class ActionPGPEncryptFiles extends ActionBase 
implements Cloneable, IAct
             vPgpFile.getActionType(),
             vPgpFile.getSourceFileFolder(),
             vPgpFile.getUserId(),
+            vPgpFile.getLocalUser(),
             vPgpFile.getDestinationFileFolder(),
             vPgpFile.getWildcard(),
             parentWorkflow,
@@ -389,6 +390,11 @@ public class ActionPGPEncryptFiles extends ActionBase 
implements Cloneable, IAct
       previousPgpFile.setWildcard(resolve(resultRow.getString(2, null)));
       previousPgpFile.setUserId(resultRow.getString(3, null));
       previousPgpFile.setDestinationFileFolder(resultRow.getString(4, null));
+      // The signing key is appended after the columns this action has always 
read, so a pipeline
+      // that still feeds five fields keeps working.
+      if (resultRow.size() > 5) {
+        previousPgpFile.setLocalUser(resultRow.getString(5, null));
+      }
 
       if (!Utils.isEmpty(previousPgpFile.getSourceFileFolder())
           && !Utils.isEmpty(previousPgpFile.getDestinationFileFolder())) {
@@ -406,6 +412,7 @@ public class ActionPGPEncryptFiles extends ActionBase 
implements Cloneable, IAct
             previousPgpFile.getActionType(),
             previousPgpFile.getSourceFileFolder(),
             previousPgpFile.getUserId(),
+            previousPgpFile.getLocalUser(),
             previousPgpFile.getDestinationFileFolder(),
             previousPgpFile.getWildcard(),
             parentWorkflow,
@@ -488,6 +495,7 @@ public class ActionPGPEncryptFiles extends ActionBase 
implements Cloneable, IAct
       ActionType actionType,
       String sourceFileFolderName,
       String userId,
+      String localUser,
       String destinationFileFolderName,
       String wildcard,
       IWorkflowEngine<WorkflowMeta> parentWorkflow,
@@ -502,9 +510,22 @@ public class ActionPGPEncryptFiles extends ActionBase 
implements Cloneable, IAct
     // Get real source, destination file and wildcard
     String realSourceFileFolderName = resolve(sourceFileFolderName);
     String realUserId = resolve(userId);
+    String realLocalUser = resolve(localUser);
     String realDestinationFileFolderName = resolve(destinationFileFolderName);
     String realWildcard = resolve(wildcard);
 
+    // Signing has no recipient, so the user ID is not used here. It never 
was: it went to gpg as
+    // -r, which GnuPG ignores for anything but encryption. Saying so out loud 
beats both the old
+    // silence and quietly promoting it to the signing key, which would change 
what an existing
+    // workflow signs with. Logged once per row, not once per file.
+    if (actionType == ActionType.SIGN
+        && Utils.isEmpty(realLocalUser)
+        && !Utils.isEmpty(realUserId)) {
+      logBasic(
+          BaseMessages.getString(
+              PKG, "ActionPGPEncryptFiles.Log.UserIdIgnoredWhenSigning", 
realUserId));
+    }
+
     try {
       sourceFileFolder = HopVfs.getFileObject(realSourceFileFolderName, 
getVariables());
       destinationFileFolder = 
HopVfs.getFileObject(realDestinationFileFolderName, getVariables());
@@ -564,6 +585,7 @@ public class ActionPGPEncryptFiles extends ActionBase 
implements Cloneable, IAct
                       shortFileName,
                       sourceFileFolder,
                       realUserId,
+                      realLocalUser,
                       destinationFile,
                       moveToFolderFolder,
                       parentWorkflow,
@@ -603,6 +625,7 @@ public class ActionPGPEncryptFiles extends ActionBase 
implements Cloneable, IAct
                       shortFileName,
                       sourceFileFolder,
                       realUserId,
+                      realLocalUser,
                       destinationfile,
                       moveToFolderFolder,
                       parentWorkflow,
@@ -660,6 +683,7 @@ public class ActionPGPEncryptFiles extends ActionBase 
implements Cloneable, IAct
                       currentFile,
                       sourceFileFolder,
                       realUserId,
+                      realLocalUser,
                       realDestinationFileFolderName,
                       realWildcard,
                       parentWorkflow,
@@ -734,6 +758,7 @@ public class ActionPGPEncryptFiles extends ActionBase 
implements Cloneable, IAct
       String shortFileName,
       FileObject sourceFileName,
       String userId,
+      String localUser,
       FileObject destinationFileName,
       FileObject moveToFolderFolder,
       IWorkflowEngine<WorkflowMeta> parentWorkflow,
@@ -744,7 +769,7 @@ public class ActionPGPEncryptFiles extends ActionBase 
implements Cloneable, IAct
     try {
       if (!destinationFileName.exists()) {
 
-        doJob(actionType, sourceFileName, userId, destinationFileName);
+        doJob(actionType, sourceFileName, userId, localUser, 
destinationFileName);
         if (isDetailed()) {
           logDetailed(
               BaseMessages.getString(
@@ -771,7 +796,7 @@ public class ActionPGPEncryptFiles extends ActionBase 
implements Cloneable, IAct
         }
         switch (ifFileExists) {
           case "overwrite_file" -> {
-            doJob(actionType, sourceFileName, userId, destinationFileName);
+            doJob(actionType, sourceFileName, userId, localUser, 
destinationFileName);
             if (isDetailed()) {
               logDetailed(
                   BaseMessages.getString(
@@ -807,7 +832,7 @@ public class ActionPGPEncryptFiles extends ActionBase 
implements Cloneable, IAct
                 destinationFileName.getParent().toString() + 
Const.FILE_SEPARATOR + shortFilename;
             destinationFile = HopVfs.getFileObject(moveToFileNameFull, 
getVariables());
 
-            doJob(actionType, sourceFileName, userId, destinationFileName);
+            doJob(actionType, sourceFileName, userId, localUser, 
destinationFileName);
             if (isDetailed()) {
               logDetailed(
                   toString(),
@@ -959,6 +984,7 @@ public class ActionPGPEncryptFiles extends ActionBase 
implements Cloneable, IAct
       FileObject currentFile,
       FileObject sourceFileFolder,
       String userId,
+      String localUser,
       String realDestinationFileFolderName,
       String realWildcard,
       IWorkflowEngine<WorkflowMeta> parentWorkflow,
@@ -1016,6 +1042,7 @@ public class ActionPGPEncryptFiles extends ActionBase 
implements Cloneable, IAct
                     shortFileName,
                     currentFile,
                     userId,
+                    localUser,
                     filename,
                     moveToFolderFolder,
                     parentWorkflow,
@@ -1033,6 +1060,7 @@ public class ActionPGPEncryptFiles extends ActionBase 
implements Cloneable, IAct
                     shortFileName,
                     currentFile,
                     userId,
+                    localUser,
                     filename,
                     moveToFolderFolder,
                     parentWorkflow,
@@ -1260,15 +1288,19 @@ public class ActionPGPEncryptFiles extends ActionBase 
implements Cloneable, IAct
   }
 
   public void doJob(
-      ActionType actionType, FileObject sourceFile, String userID, FileObject 
destinationFile)
+      ActionType actionType,
+      FileObject sourceFile,
+      String userID,
+      String localUser,
+      FileObject destinationFile)
       throws HopException {
 
     switch (actionType) {
       case SIGN:
-        gpg.signFile(sourceFile, userID, destinationFile, isAsciiMode());
+        gpg.signFile(sourceFile, localUser, destinationFile, isAsciiMode());
         break;
       case SIGN_AND_ENCRYPT:
-        gpg.signAndEncryptFile(sourceFile, userID, destinationFile, 
isAsciiMode());
+        gpg.signAndEncryptFile(sourceFile, userID, localUser, destinationFile, 
isAsciiMode());
         break;
       default:
         gpg.encryptFile(sourceFile, userID, destinationFile, isAsciiMode());
@@ -1350,6 +1382,10 @@ public class ActionPGPEncryptFiles extends ActionBase 
implements Cloneable, IAct
     @HopMetadataProperty(key = "userid")
     public String userId;
 
+    /** The key to sign with ({@code -u}). Empty leaves the choice to GnuPG. */
+    @HopMetadataProperty(key = "local_user")
+    public String localUser;
+
     @HopMetadataProperty(key = "destination_filefolder")
     public String destinationFileFolder;
 
@@ -1363,6 +1399,7 @@ public class ActionPGPEncryptFiles extends ActionBase 
implements Cloneable, IAct
       this.actionType = f.actionType;
       this.sourceFileFolder = f.sourceFileFolder;
       this.userId = f.userId;
+      this.localUser = f.localUser;
       this.destinationFileFolder = f.destinationFileFolder;
       this.wildcard = f.wildcard;
     }
diff --git 
a/plugins/actions/pgpfiles/src/main/java/org/apache/hop/workflow/actions/pgpencryptfiles/ActionPGPEncryptFilesDialog.java
 
b/plugins/actions/pgpfiles/src/main/java/org/apache/hop/workflow/actions/pgpencryptfiles/ActionPGPEncryptFilesDialog.java
index 2d31efd834..33efe7cc1d 100644
--- 
a/plugins/actions/pgpfiles/src/main/java/org/apache/hop/workflow/actions/pgpencryptfiles/ActionPGPEncryptFilesDialog.java
+++ 
b/plugins/actions/pgpfiles/src/main/java/org/apache/hop/workflow/actions/pgpencryptfiles/ActionPGPEncryptFilesDialog.java
@@ -515,6 +515,10 @@ public class ActionPGPEncryptFilesDialog extends 
ActionDialog {
               BaseMessages.getString(PKG, 
"ActionPGPEncryptFiles.Fields.UserID.Label"),
               ColumnInfo.COLUMN_TYPE_TEXT,
               false),
+          new ColumnInfo(
+              BaseMessages.getString(PKG, 
"ActionPGPEncryptFiles.Fields.LocalUser.Label"),
+              ColumnInfo.COLUMN_TYPE_TEXT,
+              false),
           new ColumnInfo(
               BaseMessages.getString(
                   PKG, 
"ActionPGPEncryptFiles.Fields.DestinationFileFolder.Label"),
@@ -529,6 +533,8 @@ public class ActionPGPEncryptFilesDialog extends 
ActionDialog {
     columnInfos[3].setToolTip(
         BaseMessages.getString(PKG, 
"ActionPGPEncryptFiles.Fields.UserID.Tooltip"));
     columnInfos[4].setToolTip(
+        BaseMessages.getString(PKG, 
"ActionPGPEncryptFiles.Fields.LocalUser.Tooltip"));
+    columnInfos[5].setToolTip(
         BaseMessages.getString(PKG, 
"ActionPGPEncryptFiles.Fields.DestinationFileFolder.Tooltip"));
 
     columnInfos[0].setUsingVariables(true);
@@ -536,6 +542,7 @@ public class ActionPGPEncryptFilesDialog extends 
ActionDialog {
     columnInfos[2].setUsingVariables(true);
     columnInfos[3].setUsingVariables(true);
     columnInfos[4].setUsingVariables(true);
+    columnInfos[5].setUsingVariables(true);
 
     wFields =
         new TableView(
@@ -566,6 +573,7 @@ public class ActionPGPEncryptFilesDialog extends 
ActionDialog {
                 wSourceFileFolder.getText(),
                 wWildcard.getText(),
                 null,
+                null,
                 wDestinationFileFolder.getText());
             wSourceFileFolder.setText("");
             wDestinationFileFolder.setText("");
@@ -598,7 +606,7 @@ public class ActionPGPEncryptFilesDialog extends 
ActionDialog {
           if (idx >= 0) {
             String[] string = wFields.getItem(idx);
             wSourceFileFolder.setText(string[1]);
-            wDestinationFileFolder.setText(string[4]);
+            wDestinationFileFolder.setText(string[5]);
             wWildcard.setText(string[2]);
             wFields.remove(idx);
           }
@@ -1548,7 +1556,8 @@ public class ActionPGPEncryptFilesDialog extends 
ActionDialog {
       ti.setText(2, Const.NVL(pgpFile.getSourceFileFolder(), ""));
       ti.setText(3, Const.NVL(pgpFile.getWildcard(), ""));
       ti.setText(4, Const.NVL(pgpFile.getUserId(), ""));
-      ti.setText(5, Const.NVL(pgpFile.getDestinationFileFolder(), ""));
+      ti.setText(5, Const.NVL(pgpFile.getLocalUser(), ""));
+      ti.setText(6, Const.NVL(pgpFile.getDestinationFileFolder(), ""));
     }
     wFields.optimizeTableView();
   }
@@ -1642,7 +1651,8 @@ public class ActionPGPEncryptFilesDialog extends 
ActionDialog {
       pgpFile.setSourceFileFolder(item.getText(2));
       pgpFile.setWildcard(item.getText(3));
       pgpFile.setUserId(item.getText(4));
-      pgpFile.setDestinationFileFolder(item.getText(5));
+      pgpFile.setLocalUser(item.getText(5));
+      pgpFile.setDestinationFileFolder(item.getText(6));
     }
     dispose();
   }
diff --git 
a/plugins/actions/pgpfiles/src/main/java/org/apache/hop/workflow/actions/pgpencryptfiles/GPG.java
 
b/plugins/actions/pgpfiles/src/main/java/org/apache/hop/workflow/actions/pgpencryptfiles/GPG.java
index db5b2096d5..9e8263759c 100644
--- 
a/plugins/actions/pgpfiles/src/main/java/org/apache/hop/workflow/actions/pgpencryptfiles/GPG.java
+++ 
b/plugins/actions/pgpfiles/src/main/java/org/apache/hop/workflow/actions/pgpencryptfiles/GPG.java
@@ -279,10 +279,28 @@ public class GPG {
     args.add(userID);
   }
 
+  /**
+   * Adds the key that signs.
+   *
+   * <p>Unlike the recipient, an empty local user is omitted rather than 
refused. GnuPG then picks
+   * the key named by {@code default-key} in {@code gpg.conf}, or the first 
usable secret key, which
+   * is what every caller got before the option existed.
+   *
+   * @param args argument list to append to
+   * @param localUser the signing key, optional
+   */
+  private static void addLocalUser(List<String> args, String localUser) {
+    if (!Utils.isEmpty(localUser)) {
+      args.add("-u");
+      args.add(localUser);
+    }
+  }
+
   /** Arguments for signing the given file with a passphrase supplied over 
stdin. */
-  private static List<String> signArgs(String filename) {
+  private static List<String> signArgs(String filename, String localUser) {
     List<String> args = new ArrayList<>();
     addPassPhraseFromStdin(args);
+    addLocalUser(args, localUser);
     args.add("--sign");
     args.add(END_OF_OPTIONS);
     args.add(filename);
@@ -393,21 +411,49 @@ public class GPG {
     }
   }
 
+  /**
+   * Sign and encrypt a file, letting GnuPG choose the signing key.
+   *
+   * @deprecated use {@link #signAndEncryptFile(FileObject, String, String, 
FileObject, boolean)},
+   *     which names the signing key as well as the recipient.
+   */
+  @Deprecated(since = "2.20")
+  public void signAndEncryptFile(
+      FileObject file, String userID, FileObject cryptedFile, boolean 
asciiMode)
+      throws HopException {
+    signAndEncryptFile(file, userID, null, cryptedFile, asciiMode);
+  }
+
   /**
    * Sign and encrypt a file
    *
    * @param file file to encrypt
    * @param userID specific user id key, required: encrypting without one 
would let GnuPG fall back
    *     to the default recipient in gpg.conf
+   * @param localUser the key to sign with, optional: without one GnuPG signs 
with the default key
+   *     from gpg.conf
    * @param cryptedFile crypted filename
    * @param asciiMode output ASCII file
    * @throws HopException
    */
   public void signAndEncryptFile(
-      FileObject file, String userID, FileObject cryptedFile, boolean 
asciiMode)
+      FileObject file, String userID, String localUser, FileObject 
cryptedFile, boolean asciiMode)
       throws HopException {
     signAndEncryptFile(
-        HopVfs.getFilename(file), userID, HopVfs.getFilename(cryptedFile), 
asciiMode);
+        HopVfs.getFilename(file), userID, localUser, 
HopVfs.getFilename(cryptedFile), asciiMode);
+  }
+
+  /**
+   * Sign and encrypt a file, letting GnuPG choose the signing key.
+   *
+   * @deprecated use {@link #signAndEncryptFile(String, String, String, 
String, boolean)}, which
+   *     names the signing key as well as the recipient.
+   */
+  @Deprecated(since = "2.20")
+  public void signAndEncryptFile(
+      String filename, String userID, String cryptedFilename, boolean 
asciiMode)
+      throws HopException {
+    signAndEncryptFile(filename, userID, null, cryptedFilename, asciiMode);
   }
 
   /**
@@ -416,12 +462,14 @@ public class GPG {
    * @param filename file to encrypt
    * @param userID specific user id key, required: encrypting without one 
would let GnuPG fall back
    *     to the default recipient in gpg.conf
+   * @param localUser the key to sign with, optional: without one GnuPG signs 
with the default key
+   *     from gpg.conf
    * @param cryptedFilename crypted filename
    * @param asciiMode output ASCII file
    * @throws HopException
    */
   public void signAndEncryptFile(
-      String filename, String userID, String cryptedFilename, boolean 
asciiMode)
+      String filename, String userID, String localUser, String 
cryptedFilename, boolean asciiMode)
       throws HopException {
 
     try {
@@ -430,6 +478,7 @@ public class GPG {
         args.add("-a");
       }
       addRecipient(args, userID);
+      addLocalUser(args, localUser);
       args.add("--output");
       args.add(cryptedFilename);
       args.add("--encrypt");
@@ -444,25 +493,27 @@ public class GPG {
   }
 
   /**
-   * Sign a file
+   * Sign a file.
    *
-   * @param filename file to encrypt
-   * @param userID specific user id key
-   * @param signedFilename crypted filename
+   * <p>The user ID is the key that signs. Signing has no recipient, so the 
value is passed as
+   * {@code -u}: until Hop 2.20 it was passed as {@code -r}, which GnuPG 
accepts and ignores for
+   * anything but encryption, so the key named here had no effect at all.
+   *
+   * @param filename file to sign
+   * @param localUser the key to sign with, optional: without one GnuPG signs 
with the default key
+   *     from gpg.conf
+   * @param signedFilename signed filename
    * @param asciiMode output ASCII file
    * @throws HopException
    */
-  public void signFile(String filename, String userID, String signedFilename, 
boolean asciiMode)
+  public void signFile(String filename, String localUser, String 
signedFilename, boolean asciiMode)
       throws HopException {
     try {
       List<String> args = new ArrayList<>(BATCH_YES);
       if (asciiMode) {
         args.add("-a");
       }
-      if (!Utils.isEmpty(userID)) {
-        args.add("-r");
-        args.add(userID);
-      }
+      addLocalUser(args, localUser);
       args.add("--output");
       args.add(signedFilename);
       args.add(asciiMode ? "--clearsign" : "--sign");
@@ -479,16 +530,17 @@ public class GPG {
   /**
    * Sign a file
    *
-   * @param file file to encrypt
-   * @param userID specific user id key
-   * @param signedFile crypted filename
+   * @param file file to sign
+   * @param localUser the key to sign with, optional: without one GnuPG signs 
with the default key
+   *     from gpg.conf
+   * @param signedFile signed filename
    * @param asciiMode output ASCII file
    * @throws HopException
    */
-  public void signFile(FileObject file, String userID, FileObject signedFile, 
boolean asciiMode)
+  public void signFile(FileObject file, String localUser, FileObject 
signedFile, boolean asciiMode)
       throws HopException {
     try {
-      signFile(HopVfs.getFilename(file), userID, 
HopVfs.getFilename(signedFile), asciiMode);
+      signFile(HopVfs.getFilename(file), localUser, 
HopVfs.getFilename(signedFile), asciiMode);
 
     } catch (Exception e) {
       throw new HopException(e);
@@ -560,23 +612,38 @@ public class GPG {
     return execGnuPG(args, plainText, false);
   }
 
+  /**
+   * Signs and encrypts a string, letting GnuPG choose the signing key.
+   *
+   * @deprecated use {@link #signAndEncrypt(String, String, String, String)}, 
which names the
+   *     signing key as well as the recipient.
+   */
+  @Deprecated(since = "2.20")
+  public String signAndEncrypt(String plainText, String userID, String 
passPhrase)
+      throws HopException {
+    return signAndEncrypt(plainText, userID, null, passPhrase);
+  }
+
   /**
    * Signs and encrypts a string
    *
    * @param plainText input string to encrypt
    * @param userID key ID of the key in GnuPG's key database to encrypt with, 
required: encrypting
    *     without one would let GnuPG fall back to the default recipient in 
gpg.conf
+   * @param localUser the key to sign with, optional: without one GnuPG signs 
with the default key
+   *     from gpg.conf
    * @param passPhrase passphrase for the personal private key to sign with
    * @return encrypted string
    * @throws HopException
    */
-  public String signAndEncrypt(String plainText, String userID, String 
passPhrase)
+  public String signAndEncrypt(String plainText, String userID, String 
localUser, String passPhrase)
       throws HopException {
     try {
       createTempFile(plainText);
 
       List<String> args = new ArrayList<>();
       addRecipient(args, userID);
+      addLocalUser(args, localUser);
       addPassPhraseFromStdin(args);
       args.add("-se");
       args.add(END_OF_OPTIONS);
@@ -589,20 +656,32 @@ public class GPG {
     }
   }
 
+  /**
+   * Signs a string, letting GnuPG choose the signing key.
+   *
+   * @deprecated use {@link #sign(String, String, String)}, which names the 
signing key.
+   */
+  @Deprecated(since = "2.20")
+  public String sign(String stringToSign, String passPhrase) throws 
HopException {
+    return sign(stringToSign, null, passPhrase);
+  }
+
   /**
    * Sign
    *
    * @param stringToSign input string to sign
+   * @param localUser the key to sign with, optional: without one GnuPG signs 
with the default key
+   *     from gpg.conf
    * @param passPhrase passphrase for the personal private key to sign with
    * @throws HopException
    */
-  public String sign(String stringToSign, String passPhrase) throws 
HopException {
+  public String sign(String stringToSign, String localUser, String passPhrase) 
throws HopException {
     String retval;
     try {
 
       createTempFile(stringToSign);
 
-      retval = execGnuPG(signArgs(getTempFileName()), passPhrase, false);
+      retval = execGnuPG(signArgs(getTempFileName(), localUser), passPhrase, 
false);
 
     } finally {
       deleteTempFile();
diff --git 
a/plugins/actions/pgpfiles/src/main/resources/org/apache/hop/workflow/actions/pgpencryptfiles/messages/messages_en_US.properties
 
b/plugins/actions/pgpfiles/src/main/resources/org/apache/hop/workflow/actions/pgpencryptfiles/messages/messages_en_US.properties
index 56c797d481..7cd439fed6 100644
--- 
a/plugins/actions/pgpfiles/src/main/resources/org/apache/hop/workflow/actions/pgpencryptfiles/messages/messages_en_US.properties
+++ 
b/plugins/actions/pgpfiles/src/main/resources/org/apache/hop/workflow/actions/pgpencryptfiles/messages/messages_en_US.properties
@@ -70,10 +70,12 @@ ActionPGPEncryptFiles.Fields.Action.Label=Action
 ActionPGPEncryptFiles.Fields.DestinationFileFolder.Label=File/Folder 
destination
 ActionPGPEncryptFiles.Fields.DestinationFileFolder.Tooltip=Enter here the 
destination folder to hit.\n If you selected file as source,you can define a 
file as destination.
 ActionPGPEncryptFiles.Fields.Label=Files/Folders\: 
+ActionPGPEncryptFiles.Fields.LocalUser.Label=Signing key
+ActionPGPEncryptFiles.Fields.LocalUser.Tooltip=User ID of the key to sign with 
(gpg ''-u'').\n Leave empty to let GnuPG use the default key from its 
configuration.\n Ignored when the action only encrypts.
 ActionPGPEncryptFiles.Fields.SourceFileFolder.Label=File/Folder source
 ActionPGPEncryptFiles.Fields.SourceFileFolder.Tooltip=Enter here the file or 
folder to move\n If it's a folder, Apache Hop will fetch only if ''Include 
subfolders'' is checked\!
 ActionPGPEncryptFiles.Fields.UserID.Label=User ID
-ActionPGPEncryptFiles.Fields.UserID.Tooltip=Name of the key
+ActionPGPEncryptFiles.Fields.UserID.Tooltip=User ID of the key to encrypt to 
(gpg ''-r'').\n Required when the action encrypts.
 ActionPGPEncryptFiles.Fields.Wildcard.Label=Wildcard
 ActionPGPEncryptFiles.Fields.Wildcard.Tooltip=Specify here the wildcard to 
match.\n Only files that match the wildcard will be moved.
 ActionPGPEncryptFiles.FilenameAdd.Button=&Add
@@ -114,6 +116,7 @@ ActionPGPEncryptFiles.Log.IncludeSubFoldersOn=Sub folders 
will be fetched ...
 ActionPGPEncryptFiles.Log.Info.FilesInError=Total files in error \: {0}
 ActionPGPEncryptFiles.Log.Info.FilesInSuccess=Total files successfully 
processed \: {0}
 ActionPGPEncryptFiles.Log.ProcessingRow=Processing row source File/folder 
source \: [{0}] ... destination file/folder \: [{1}]... wildcard \: [{2}]
+ActionPGPEncryptFiles.Log.UserIdIgnoredWhenSigning=The User ID ''{0}'' is not 
used by the Sign action\: it is the key to encrypt to. Fill in the Signing key 
to choose the key to sign with.
 ActionPGPEncryptFiles.Move_To_Folder_IfFileExists.Label=Move source file to 
folder
 ActionPGPEncryptFiles.MovedDateTimeFormat.Label=Date format
 ActionPGPEncryptFiles.Name=Encrypt files with PGP
@@ -124,7 +127,7 @@ ActionPGPEncryptFiles.NrErrorsLessThan.Tooltip=Success when 
Number of errors is
 ActionPGPEncryptFiles.Overwrite_File_IfFileExists.Label=Overwrite destination 
file
 ActionPGPEncryptFiles.Overwrite_Filename_IffMovedFileExists.Label=Overwrite 
file
 ActionPGPEncryptFiles.Previous.Label=Copy previous results to args
-ActionPGPEncryptFiles.Previous.Tooltip=Check this to pass the results of the 
previous entry to the arguments of this entry.\nBe careful, arguments must be 
in the same order that arguments\!\n ie \:\n(1) action type (encrypt, sign, 
sign and encrypt)\n(2) source folder/file\n(3) wildcard\n(4) User ID\n(5) 
destination folder/file
+ActionPGPEncryptFiles.Previous.Tooltip=Check this to pass the results of the 
previous entry to the arguments of this entry.\nBe careful, arguments must be 
in the same order that arguments\!\n ie \:\n(1) action type (encrypt, sign, 
sign and encrypt)\n(2) source folder/file\n(3) wildcard\n(4) User ID\n(5) 
destination folder/file\n(6) signing key (optional)\nThe signing key comes 
after the destination, not where the Signing key column sits in the grid.
 ActionPGPEncryptFiles.Settings.Label=Settings
 ActionPGPEncryptFiles.SourceFileFolder.Label=File/Folder source
 ActionPGPEncryptFiles.SourceFileFolder.Tooltip=Enter here the file or folder 
to move\n If it's a folder, check ''Include subfolders'' option if necessary\!
diff --git 
a/plugins/actions/pgpfiles/src/test/java/org/apache/hop/workflow/actions/pgpencryptfiles/ActionPGPEncryptFilesDialogTest.java
 
b/plugins/actions/pgpfiles/src/test/java/org/apache/hop/workflow/actions/pgpencryptfiles/ActionPGPEncryptFilesDialogTest.java
index 3ac63d6083..77c0baa5c8 100644
--- 
a/plugins/actions/pgpfiles/src/test/java/org/apache/hop/workflow/actions/pgpencryptfiles/ActionPGPEncryptFilesDialogTest.java
+++ 
b/plugins/actions/pgpfiles/src/test/java/org/apache/hop/workflow/actions/pgpencryptfiles/ActionPGPEncryptFilesDialogTest.java
@@ -52,12 +52,14 @@ class ActionPGPEncryptFilesDialogTest extends 
SwtBotTestBase {
   private static final String SIGN_SOURCE = "/data/outbox/invoices.csv";
 
   private static final String SIGN_WILDCARD = ".*\\.csv$";
-  private static final String SIGN_USER_ID = "[email protected]";
+  private static final String SIGN_USER_ID = "[email protected]";
+  private static final String SIGN_LOCAL_USER = "[email protected]";
   private static final String SIGN_DESTINATION = 
"/data/signed/invoices.csv.asc";
 
   private static final String SEAL_SOURCE = "/data/outbox/payments.xml";
   private static final String SEAL_WILDCARD = ".*\\.xml$";
   private static final String SEAL_USER_ID = "[email protected]";
+  private static final String SEAL_LOCAL_USER = "[email protected]";
   private static final String SEAL_DESTINATION = 
"/data/sealed/payments.xml.gpg";
 
   @Test
@@ -72,7 +74,8 @@ class ActionPGPEncryptFilesDialogTest extends SwtBotTestBase {
     assertEquals(ActionPGPEncryptFiles.ActionType.SIGN, 
signRow.getActionType(), "action type");
     assertEquals(SIGN_SOURCE, signRow.getSourceFileFolder(), "source 
file/folder");
     assertEquals(SIGN_WILDCARD, signRow.getWildcard(), "wildcard");
-    assertEquals(SIGN_USER_ID, signRow.getUserId(), "user id (the key to sign 
with)");
+    assertEquals(SIGN_USER_ID, signRow.getUserId(), "user id (the key to 
encrypt to)");
+    assertEquals(SIGN_LOCAL_USER, signRow.getLocalUser(), "local user (the key 
to sign with)");
     assertEquals(SIGN_DESTINATION, signRow.getDestinationFileFolder(), 
"destination file/folder");
 
     ActionPGPEncryptFiles.PgpFile sealRow = action.getPgpFiles().get(1);
@@ -80,7 +83,8 @@ class ActionPGPEncryptFilesDialogTest extends SwtBotTestBase {
         ActionPGPEncryptFiles.ActionType.SIGN_AND_ENCRYPT, 
sealRow.getActionType(), "action type");
     assertEquals(SEAL_SOURCE, sealRow.getSourceFileFolder(), "source 
file/folder");
     assertEquals(SEAL_WILDCARD, sealRow.getWildcard(), "wildcard");
-    assertEquals(SEAL_USER_ID, sealRow.getUserId(), "user id (the key to sign 
with)");
+    assertEquals(SEAL_USER_ID, sealRow.getUserId(), "user id (the key to 
encrypt to)");
+    assertEquals(SEAL_LOCAL_USER, sealRow.getLocalUser(), "local user (the key 
to sign with)");
     assertEquals(SEAL_DESTINATION, sealRow.getDestinationFileFolder(), 
"destination file/folder");
   }
 
@@ -99,6 +103,7 @@ class ActionPGPEncryptFilesDialogTest extends SwtBotTestBase 
{
           typeInto(grid, 0, headers.indexOf(label("SourceFileFolder")), 
"typed-under-source");
           typeInto(grid, 0, headers.indexOf(label("Wildcard")), 
"typed-under-wildcard");
           typeInto(grid, 0, headers.indexOf(label("UserID")), 
"typed-under-user-id");
+          typeInto(grid, 0, headers.indexOf(label("LocalUser")), 
"typed-under-local-user");
           typeInto(grid, 0, headers.indexOf(label("DestinationFileFolder")), 
"typed-under-dest");
         });
 
@@ -106,6 +111,7 @@ class ActionPGPEncryptFilesDialogTest extends 
SwtBotTestBase {
     assertEquals("typed-under-source", row.getSourceFileFolder(), 
label("SourceFileFolder"));
     assertEquals("typed-under-wildcard", row.getWildcard(), label("Wildcard"));
     assertEquals("typed-under-user-id", row.getUserId(), label("UserID"));
+    assertEquals("typed-under-local-user", row.getLocalUser(), 
label("LocalUser"));
     assertEquals(
         "typed-under-dest", row.getDestinationFileFolder(), 
label("DestinationFileFolder"));
   }
@@ -146,6 +152,7 @@ class ActionPGPEncryptFilesDialogTest extends 
SwtBotTestBase {
                 SIGN_SOURCE,
                 SIGN_WILDCARD,
                 SIGN_USER_ID,
+                SIGN_LOCAL_USER,
                 SIGN_DESTINATION));
     action
         .getPgpFiles()
@@ -155,6 +162,7 @@ class ActionPGPEncryptFilesDialogTest extends 
SwtBotTestBase {
                 SEAL_SOURCE,
                 SEAL_WILDCARD,
                 SEAL_USER_ID,
+                SEAL_LOCAL_USER,
                 SEAL_DESTINATION));
     return action;
   }
@@ -164,12 +172,14 @@ class ActionPGPEncryptFilesDialogTest extends 
SwtBotTestBase {
       String source,
       String wildcard,
       String userId,
+      String localUser,
       String destination) {
     ActionPGPEncryptFiles.PgpFile file = new ActionPGPEncryptFiles.PgpFile();
     file.setActionType(actionType);
     file.setSourceFileFolder(source);
     file.setWildcard(wildcard);
     file.setUserId(userId);
+    file.setLocalUser(localUser);
     file.setDestinationFileFolder(destination);
     return file;
   }
diff --git 
a/plugins/actions/pgpfiles/src/test/java/org/apache/hop/workflow/actions/pgpencryptfiles/ActionPGPEncryptFilesSignTest.java
 
b/plugins/actions/pgpfiles/src/test/java/org/apache/hop/workflow/actions/pgpencryptfiles/ActionPGPEncryptFilesSignTest.java
index cb9e212779..3929ca6bd3 100644
--- 
a/plugins/actions/pgpfiles/src/test/java/org/apache/hop/workflow/actions/pgpencryptfiles/ActionPGPEncryptFilesSignTest.java
+++ 
b/plugins/actions/pgpfiles/src/test/java/org/apache/hop/workflow/actions/pgpencryptfiles/ActionPGPEncryptFilesSignTest.java
@@ -18,6 +18,8 @@
 package org.apache.hop.workflow.actions.pgpencryptfiles;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.junit.jupiter.api.Assumptions.assumeTrue;
 import static org.mockito.Mockito.mock;
@@ -64,6 +66,10 @@ import org.junit.jupiter.api.condition.OS;
 class ActionPGPEncryptFilesSignTest {
 
   private static final String KEY_USER_ID = "[email protected]";
+
+  /** A second key in the same keyring, so "which key signed this" has more 
than one answer. */
+  private static final String SIGNER_USER_ID = "[email protected]";
+
   private static final String PLAIN_TEXT = "Apache Hop signs this file.\n";
 
   private static Path gpgBinary;
@@ -101,8 +107,13 @@ class ActionPGPEncryptFilesSignTest {
         StandardCharsets.UTF_8);
     Files.setPosixFilePermissions(gpgWrapper, 
PosixFilePermissions.fromString("rwx------"));
 
-    // A passphrase-less key: the encrypt action has no passphrase field, so 
signing can only ever
+    // Passphrase-less keys: the encrypt action has no passphrase field, so 
signing can only ever
     // use a key gpg can unlock on its own.
+    generateKey("Hop PGP Test <" + KEY_USER_ID + ">");
+    generateKey("Hop PGP Signer <" + SIGNER_USER_ID + ">");
+  }
+
+  private void generateKey(String userId) throws Exception {
     run(
         gpgBinary.toString(),
         "--homedir",
@@ -114,7 +125,7 @@ class ActionPGPEncryptFilesSignTest {
         "--passphrase",
         "",
         "--quick-generate-key",
-        "Hop PGP Test <" + KEY_USER_ID + ">",
+        userId,
         "default",
         "default",
         "never");
@@ -142,8 +153,7 @@ class ActionPGPEncryptFilesSignTest {
                 ActionPGPEncryptFiles.ActionType.SIGN,
                 source,
                 signed,
-                // The key to sign with cannot be chosen: the User ID goes to 
gpg as -r, which
-                // --clearsign ignores. See 
https://github.com/apache/hop/issues/8206.
+                // No key named: gpg signs with its default key.
                 ""));
 
     Result result = sign.execute(new Result(), 0);
@@ -204,6 +214,130 @@ class ActionPGPEncryptFilesSignTest {
     assertEquals(PLAIN_TEXT, Files.readString(opened), "the round trip must 
preserve the content");
   }
 
+  /**
+   * With two usable secret keys in the keyring, gpg's own default can only be 
one of them. Naming
+   * either one on the row has to produce a signature from that key, which is 
what
+   * https://github.com/apache/hop/issues/8659 asked for.
+   */
+  @Test
+  void theSigningKeyIsChosenPerRow() throws Exception {
+    assertSignedBy(SIGNER_USER_ID, sign(SIGNER_USER_ID, 
"signed-by-signer.csv"));
+    assertSignedBy(KEY_USER_ID, sign(KEY_USER_ID, "signed-by-test.csv"));
+  }
+
+  /**
+   * A Sign row written before the signing key existed could only name a User 
ID, and that went to
+   * gpg as {@code -r}, which it ignores outside encryption: such a row has 
always signed with the
+   * default key. It still has to, or upgrading Hop would silently change what 
those workflows sign
+   * with, and fail outright where the named key has no secret half in the 
keyring.
+   */
+  @Test
+  void aUserIdOnItsOwnStillSignsWithTheDefaultKey() throws Exception {
+    Path source = Files.writeString(work.resolve("legacy.csv"), PLAIN_TEXT);
+    Path signed = work.resolve("legacy.csv.asc");
+
+    ActionPGPEncryptFiles action = encryptAction();
+    action.setAsciiMode(true);
+    action
+        .getPgpFiles()
+        .add(
+            pgpFile(
+                ActionPGPEncryptFiles.ActionType.SIGN,
+                source,
+                signed,
+                // The only field such a row has, and never the key that signs.
+                SIGNER_USER_ID,
+                null));
+
+    Result result = action.execute(new Result(), 0);
+
+    assertEquals(0, result.getNrErrors(), "signing must not report errors");
+    assertTrue(result.getResult(), "signing must succeed");
+
+    // Only meaningful while the two differ: were gpg's default the key this 
row names, the
+    // assertion below would hold whether or not the User ID had been promoted 
to the signing key.
+    String defaultKey = defaultSigningKey();
+    assertNotEquals(
+        SIGNER_USER_ID,
+        defaultKey,
+        "gpg's default key is the one this row names, so this test would prove 
nothing");
+    assertSignedBy(defaultKey, signed);
+  }
+
+  /** The key gpg signs with when nothing names one: whichever of the two it 
picks on its own. */
+  private String defaultSigningKey() throws Exception {
+    Path probe = Files.writeString(work.resolve("probe.csv"), PLAIN_TEXT);
+    Path signed = work.resolve("probe.csv.asc");
+
+    ActionPGPEncryptFiles action = encryptAction();
+    action.setAsciiMode(true);
+    action
+        .getPgpFiles()
+        .add(pgpFile(ActionPGPEncryptFiles.ActionType.SIGN, probe, signed, "", 
null));
+    assertEquals(0, action.execute(new Result(), 0).getNrErrors(), "the probe 
must sign");
+
+    String report = gpg("--verify", signed.toString());
+    return report.contains(SIGNER_USER_ID) ? SIGNER_USER_ID : KEY_USER_ID;
+  }
+
+  /** Sealing to one key while signing with another is what the two options 
are for. */
+  @Test
+  void signAndEncryptSealsToTheUserIdAndSignsWithTheLocalUser() throws 
Exception {
+    Path source = Files.writeString(work.resolve("statement.xml"), PLAIN_TEXT);
+    Path sealed = work.resolve("statement.xml.asc");
+
+    ActionPGPEncryptFiles seal = encryptAction();
+    seal.setAsciiMode(true);
+    seal.getPgpFiles()
+        .add(
+            pgpFile(
+                ActionPGPEncryptFiles.ActionType.SIGN_AND_ENCRYPT,
+                source,
+                sealed,
+                KEY_USER_ID,
+                SIGNER_USER_ID));
+
+    Result result = seal.execute(new Result(), 0);
+
+    assertEquals(0, result.getNrErrors(), "sign and encrypt must not report 
errors");
+    assertTrue(result.getResult(), "sign and encrypt must succeed");
+
+    // Decrypting reports the signature as well, so one run shows both halves 
of the choice.
+    String report = gpg("--decrypt", sealed.toString());
+    assertTrue(
+        report.contains(SIGNER_USER_ID),
+        "the signature must come from the key named as the signing key:\n" + 
report);
+    assertTrue(report.contains(PLAIN_TEXT), "decrypting must return the 
content:\n" + report);
+  }
+
+  /** Signs a file with the given key and returns the signature. */
+  private Path sign(String localUser, String name) throws Exception {
+    Path source = Files.writeString(work.resolve(name), PLAIN_TEXT);
+    Path signed = work.resolve(name + ".asc");
+
+    ActionPGPEncryptFiles action = encryptAction();
+    action.setAsciiMode(true);
+    action
+        .getPgpFiles()
+        .add(pgpFile(ActionPGPEncryptFiles.ActionType.SIGN, source, signed, 
"", localUser));
+
+    Result result = action.execute(new Result(), 0);
+
+    assertEquals(0, result.getNrErrors(), "signing with " + localUser + " must 
not report errors");
+    assertTrue(result.getResult(), "signing with " + localUser + " must 
succeed");
+    return signed;
+  }
+
+  private void assertSignedBy(String expectedUserId, Path signed) throws 
Exception {
+    String other = expectedUserId.equals(SIGNER_USER_ID) ? KEY_USER_ID : 
SIGNER_USER_ID;
+    String report = gpg("--verify", signed.toString());
+    assertTrue(
+        report.contains(expectedUserId),
+        "the signature must come from " + expectedUserId + ":\n" + report);
+    assertFalse(
+        report.contains(other), "the signature must not come from " + other + 
":\n" + report);
+  }
+
   /**
    * Filenames are discovered by scanning a folder, so their content is chosen 
by whoever can write
    * to it. They must reach gpg as literal arguments and never be interpreted.
@@ -252,11 +386,21 @@ class ActionPGPEncryptFilesSignTest {
 
   private static ActionPGPEncryptFiles.PgpFile pgpFile(
       ActionPGPEncryptFiles.ActionType actionType, Path source, Path 
destination, String userId) {
+    return pgpFile(actionType, source, destination, userId, null);
+  }
+
+  private static ActionPGPEncryptFiles.PgpFile pgpFile(
+      ActionPGPEncryptFiles.ActionType actionType,
+      Path source,
+      Path destination,
+      String userId,
+      String localUser) {
     ActionPGPEncryptFiles.PgpFile file = new ActionPGPEncryptFiles.PgpFile();
     file.setActionType(actionType);
     file.setSourceFileFolder(source.toString());
     file.setDestinationFileFolder(destination.toString());
     file.setUserId(userId);
+    file.setLocalUser(localUser);
     return file;
   }
 
@@ -295,6 +439,21 @@ class ActionPGPEncryptFilesSignTest {
     return null;
   }
 
+  /** Runs gpg against the throwaway keyring and returns what it reported, 
stderr included. */
+  private String gpg(String... arguments) throws Exception {
+    List<String> command = new java.util.ArrayList<>();
+    command.add(gpgBinary.toString());
+    command.add("--homedir");
+    command.add(gnupgHome.toString());
+    command.addAll(List.of(arguments));
+
+    Process process = new 
ProcessBuilder(command).redirectErrorStream(true).start();
+    String output = new String(process.getInputStream().readAllBytes(), 
StandardCharsets.UTF_8);
+    assertTrue(process.waitFor(60, TimeUnit.SECONDS), "timed out: " + 
String.join(" ", command));
+    assertEquals(0, process.exitValue(), String.join(" ", command) + " 
failed:\n" + output);
+    return output;
+  }
+
   private static void run(String... command) throws Exception {
     Process process = new 
ProcessBuilder(command).redirectErrorStream(true).start();
     String output = new String(process.getInputStream().readAllBytes(), 
StandardCharsets.UTF_8);
diff --git 
a/plugins/actions/pgpfiles/src/test/java/org/apache/hop/workflow/actions/pgpencryptfiles/ActionPGPEncryptFilesTest.java
 
b/plugins/actions/pgpfiles/src/test/java/org/apache/hop/workflow/actions/pgpencryptfiles/ActionPGPEncryptFilesTest.java
index 5da97d70b1..dc055fc07d 100644
--- 
a/plugins/actions/pgpfiles/src/test/java/org/apache/hop/workflow/actions/pgpencryptfiles/ActionPGPEncryptFilesTest.java
+++ 
b/plugins/actions/pgpfiles/src/test/java/org/apache/hop/workflow/actions/pgpencryptfiles/ActionPGPEncryptFilesTest.java
@@ -19,6 +19,7 @@
 package org.apache.hop.workflow.actions.pgpencryptfiles;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import org.apache.hop.core.encryption.Encr;
@@ -74,18 +75,22 @@ class ActionPGPEncryptFilesTest {
     assertEquals(ActionPGPEncryptFiles.ActionType.ENCRYPT, f.getActionType());
     assertEquals("folder1", f.getSourceFileFolder());
     assertEquals("user1", f.getUserId());
+    // Written before the signing key had a field of its own, so it has none.
+    assertNull(f.getLocalUser());
     assertEquals("target1", f.getDestinationFileFolder());
     assertEquals("wildcard1", f.getWildcard());
     f = action.getPgpFiles().get(1);
     assertEquals(ActionPGPEncryptFiles.ActionType.SIGN, f.getActionType());
     assertEquals("folder2", f.getSourceFileFolder());
     assertEquals("user2", f.getUserId());
+    assertEquals("signer2", f.getLocalUser());
     assertEquals("target2", f.getDestinationFileFolder());
     assertEquals("wildcard2", f.getWildcard());
     f = action.getPgpFiles().getLast();
     assertEquals(ActionPGPEncryptFiles.ActionType.SIGN_AND_ENCRYPT, 
f.getActionType());
     assertEquals("folder3", f.getSourceFileFolder());
     assertEquals("user3", f.getUserId());
+    assertEquals("signer3", f.getLocalUser());
     assertEquals("target3", f.getDestinationFileFolder());
     assertEquals("wildcard3", f.getWildcard());
   }
diff --git 
a/plugins/actions/pgpfiles/src/test/java/org/apache/hop/workflow/actions/pgpencryptfiles/GpgArgumentPassingTest.java
 
b/plugins/actions/pgpfiles/src/test/java/org/apache/hop/workflow/actions/pgpencryptfiles/GpgArgumentPassingTest.java
index 7ec302f877..8857ae9940 100644
--- 
a/plugins/actions/pgpfiles/src/test/java/org/apache/hop/workflow/actions/pgpencryptfiles/GpgArgumentPassingTest.java
+++ 
b/plugins/actions/pgpfiles/src/test/java/org/apache/hop/workflow/actions/pgpencryptfiles/GpgArgumentPassingTest.java
@@ -141,7 +141,7 @@ class GpgArgumentPassingTest {
   @Test
   void signAndEncryptFilePassesFilenamesLiterally() throws Exception {
     for (String name : HOSTILE_NAMES) {
-      gpg().signAndEncryptFile(name, "[email protected]", "sealed-" + name, 
true);
+      gpg().signAndEncryptFile(name, "[email protected]", null, "sealed-" + 
name, true);
       assertPassedLiterally(name, "signAndEncryptFile source");
       assertPassedLiterally("sealed-" + name, "signAndEncryptFile 
destination");
       assertRecipient("[email protected]", "signAndEncryptFile");
@@ -199,7 +199,7 @@ class GpgArgumentPassingTest {
 
   @Test
   void signAndEncryptStringSendsThePassphraseOverStdin() throws Exception {
-    gpg().signAndEncrypt("some data", "[email protected]", PASSPHRASE);
+    gpg().signAndEncrypt("some data", "[email protected]", null, PASSPHRASE);
     assertPassphraseOnStdinOnly("signAndEncrypt");
   }
 
@@ -220,15 +220,60 @@ class GpgArgumentPassingTest {
     assertEquals("", recordedStdin(), "nothing must be written to stdin 
without a passphrase");
   }
 
+  /**
+   * Signing has no recipient. Until Hop 2.20 the key named on a SIGN row was 
passed as {@code -r},
+   * which GnuPG accepts and ignores for anything but encryption, so the 
choice had no effect.
+   */
   @Test
-  void anEmptyUserIdOmitsTheRecipientFlagWhenSigning() throws Exception {
-    gpg().signFile("plain.txt", "", "plain.txt.asc", true);
+  void signingNamesTheKeyWithTheLocalUserFlag() throws Exception {
+    gpg().signFile("plain.txt", "[email protected]", "plain.txt.asc", true);
+    assertLocalUser("[email protected]", "signFile");
     assertFalse(
         recordedArguments().contains("-r"),
-        "an empty user id must not be passed to GnuPG as an empty recipient");
+        "signing has no recipient, so the key must not be passed as one: " + 
recordedArguments());
+  }
 
-    gpg().signFile("plain.txt", "[email protected]", "plain.txt.asc", true);
-    assertRecipient("[email protected]", "signFile");
+  @Test
+  void anEmptySigningKeyIsOmittedRatherThanPassedEmpty() throws Exception {
+    gpg().signFile("plain.txt", "", "plain.txt.asc", true);
+    List<String> args = recordedArguments();
+    assertFalse(args.contains("-u"), "an empty signing key must not reach 
GnuPG: " + args);
+    assertFalse(args.contains("-r"), "signing must never pass a recipient: " + 
args);
+  }
+
+  /**
+   * Sealing to one key and signing with another is the whole point of having 
both options: the two
+   * user IDs have to land on their own flags and stay distinct.
+   */
+  @Test
+  void signAndEncryptCarriesTheRecipientAndTheSigningKeySeparately() throws 
Exception {
+    gpg()
+        .signAndEncryptFile(
+            "plain.txt", "[email protected]", "[email protected]", 
"sealed.asc", true);
+    assertRecipient("[email protected]", "signAndEncryptFile");
+    assertLocalUser("[email protected]", "signAndEncryptFile");
+  }
+
+  @Test
+  void signAndEncryptWithoutASigningKeyLeavesTheChoiceToGnuPg() throws 
Exception {
+    gpg().signAndEncryptFile("plain.txt", "[email protected]", null, 
"sealed.asc", true);
+    assertRecipient("[email protected]", "signAndEncryptFile");
+    assertFalse(
+        recordedArguments().contains("-u"),
+        "no signing key must mean no -u, so GnuPG falls back to its default 
key");
+  }
+
+  @Test
+  void signAndEncryptStringCarriesTheSigningKey() throws Exception {
+    gpg().signAndEncrypt("some data", "[email protected]", 
"[email protected]", PASSPHRASE);
+    assertRecipient("[email protected]", "signAndEncrypt");
+    assertLocalUser("[email protected]", "signAndEncrypt");
+  }
+
+  @Test
+  void signStringCarriesTheSigningKey() throws Exception {
+    gpg().sign("some data", "[email protected]", PASSPHRASE);
+    assertLocalUser("[email protected]", "sign");
   }
 
   /**
@@ -244,7 +289,7 @@ class GpgArgumentPassingTest {
         "encryptFile must refuse an empty recipient");
     assertThrows(
         HopException.class,
-        () -> gpg.signAndEncryptFile("plain.txt", "", "sealed.asc", false),
+        () -> gpg.signAndEncryptFile("plain.txt", "", null, "sealed.asc", 
false),
         "signAndEncryptFile must refuse an empty recipient");
     assertThrows(
         HopException.class,
@@ -252,7 +297,7 @@ class GpgArgumentPassingTest {
         "encrypt must refuse an empty recipient");
     assertThrows(
         HopException.class,
-        () -> gpg.signAndEncrypt("some data", "", PASSPHRASE),
+        () -> gpg.signAndEncrypt("some data", "", null, PASSPHRASE),
         "signAndEncrypt must refuse an empty recipient");
 
     assertFalse(Files.exists(record), "GnuPG must not be started without a 
recipient");
@@ -322,7 +367,7 @@ class GpgArgumentPassingTest {
     gpg().encrypt("some data", payload);
     assertFalse(Files.exists(marker), "encrypt executed a command from a key 
id");
 
-    gpg().signAndEncrypt("some data", payload, PASSPHRASE);
+    gpg().signAndEncrypt("some data", payload, null, PASSPHRASE);
     assertFalse(Files.exists(marker), "signAndEncrypt executed a command from 
a key id");
 
     assertPassedLiterally(payload, "the payload");
@@ -374,6 +419,15 @@ class GpgArgumentPassingTest {
     assertEquals(PASSPHRASE, recordedStdin(), what + " must write the 
passphrase to stdin");
   }
 
+  private void assertLocalUser(String expected, String what) throws 
IOException {
+    List<String> args = recordedArguments();
+    assertTrue(args.contains("-u"), what + " must pass a signing key: " + 
args);
+    assertEquals(
+        expected,
+        args.get(args.indexOf("-u") + 1),
+        what + " must pass the signing key as its own argument following -u: " 
+ args);
+  }
+
   private void assertRecipient(String expected, String what) throws 
IOException {
     List<String> args = recordedArguments();
     assertTrue(args.contains("-r"), what + " must pass a recipient: " + args);
diff --git 
a/plugins/actions/pgpfiles/src/test/resources/action-pgp-encrypt-files.xml 
b/plugins/actions/pgpfiles/src/test/resources/action-pgp-encrypt-files.xml
index 70b2071cce..d9a3a4f0eb 100644
--- a/plugins/actions/pgpfiles/src/test/resources/action-pgp-encrypt-files.xml
+++ b/plugins/actions/pgpfiles/src/test/resources/action-pgp-encrypt-files.xml
@@ -53,6 +53,7 @@
             <action_type>sign</action_type>
             <source_filefolder>folder2</source_filefolder>
             <userid>user2</userid>
+            <local_user>signer2</local_user>
             <destination_filefolder>target2</destination_filefolder>
             <wildcard>wildcard2</wildcard>
         </field>
@@ -60,6 +61,7 @@
             <action_type>signandencrypt</action_type>
             <source_filefolder>folder3</source_filefolder>
             <userid>user3</userid>
+            <local_user>signer3</local_user>
             <destination_filefolder>target3</destination_filefolder>
             <wildcard>wildcard3</wildcard>
         </field>

Reply via email to