You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@nifi.apache.org by sz...@apache.org on 2026/09/28 17:36:53 UTC

(nifi-minifi-cpp) branch main updated: MINIFICPP-2749 Add EncryptContentPGP and DecryptContentPGP

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

szaszm pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/nifi-minifi-cpp.git


The following commit(s) were added to refs/heads/main by this push:
     new 7148ad2a5 MINIFICPP-2749 Add EncryptContentPGP and DecryptContentPGP
7148ad2a5 is described below

commit 7148ad2a50d65af8de5790ec0c6cbc2f5a51774a
Author: Martin Zink <ma...@apache.org>
AuthorDate: Mon Sep 28 17:41:34 2026 +0200

    MINIFICPP-2749 Add EncryptContentPGP and DecryptContentPGP
    
    Closes #2225
    
    Signed-off-by: Marton Szasz <sz...@apache.org>
---
 .../ubuntu_22_04_clang_arm_manifest.json           | 178 ++++++++
 CONTROLLERS.md                                     |  36 +-
 PROCESSORS.md                                      |  56 +++
 README.md                                          |  53 +--
 cmake/MiNiFiOptions.cmake                          |   1 +
 minifi_rust/CMakeLists.txt                         |  10 +
 minifi_rust/Cargo.toml                             |   6 +
 .../extensions/minifi_pgp/.cargo/config.toml       |   5 +
 minifi_rust/extensions/minifi_pgp/.gitignore       |   7 +
 minifi_rust/extensions/minifi_pgp/Cargo.toml       |  14 +
 .../minifi_pgp/features/encrypt_decrypt.feature    |  67 +++
 .../extensions/minifi_pgp/features/environment.py  |  41 ++
 .../extensions/minifi_pgp/features/steps/steps.py  |  80 ++++
 minifi_rust/extensions/minifi_pgp/minifi_pgp.md    | 112 +++++
 .../src/controller_services/encryption_key.rs      | 131 ++++++
 .../src/controller_services/key_file_property.rs   | 118 +++++
 .../src/controller_services/key_lookup.rs          | 141 ++++++
 .../src/controller_services/key_parsing.rs         |  90 ++++
 .../src/controller_services/key_property.rs        |  49 +++
 .../minifi_pgp/src/controller_services/mod.rs      |  24 ++
 .../src/controller_services/private_key_service.rs | 249 +++++++++++
 .../src/controller_services/public_key_service.rs  | 296 +++++++++++++
 minifi_rust/extensions/minifi_pgp/src/lib.rs       |  41 ++
 .../minifi_pgp/src/processors/decrypt_content.rs   | 474 +++++++++++++++++++++
 .../minifi_pgp/src/processors/encrypt_content.rs   | 392 +++++++++++++++++
 .../extensions/minifi_pgp/src/processors/mod.rs    |  19 +
 .../extensions/minifi_pgp/src/test_utils/mod.rs    |  32 ++
 minifi_rust/extensions/minifi_pgp/src/utils.rs     |  80 ++++
 .../extensions/minifi_pgp/test_keys/README.txt     |  59 +++
 .../extensions/minifi_pgp/test_keys/alice.asc      |  18 +
 .../extensions/minifi_pgp/test_keys/alice.gpg      | Bin 0 -> 640 bytes
 .../minifi_pgp/test_keys/alice_private.asc         |  32 ++
 .../minifi_pgp/test_keys/alice_private.gpg         | Bin 0 -> 1337 bytes
 .../minifi_pgp/test_keys/ambiguous_keyring.gpg     | Bin 0 -> 3227 bytes
 .../minifi_pgp/test_keys/bob_private.asc           |  71 +++
 .../minifi_pgp/test_keys/bob_private.gpg           | Bin 0 -> 3207 bytes
 .../extensions/minifi_pgp/test_keys/dave.asc       |  14 +
 .../minifi_pgp/test_keys/dave_private.asc          |  18 +
 .../extensions/minifi_pgp/test_keys/erin.asc       |  10 +
 .../extensions/minifi_pgp/test_keys/garbage.gpg    | Bin 0 -> 1024 bytes
 .../extensions/minifi_pgp/test_keys/keyring.asc    |  58 +++
 .../extensions/minifi_pgp/test_keys/keyring.gpg    | Bin 0 -> 2545 bytes
 .../minifi_pgp/test_keys/mixed_secret_keyring.gpg  | Bin 0 -> 1966 bytes
 .../minifi_pgp/test_keys/secret_keyring.asc        |  99 +++++
 .../minifi_pgp/test_keys/secret_keyring.gpg        | Bin 0 -> 4544 bytes
 .../minifi_pgp/test_keys/spoofed_bob.asc           |  19 +
 .../extensions/minifi_pgp/test_keys/truncated.asc  |  10 +
 .../minifi_pgp/test_keys/truncated_private.asc     |  17 +
 .../minifi_pgp/test_messages/foo_for_alice.asc     |  12 +
 .../minifi_pgp/test_messages/foo_for_alice.gpg     | Bin 0 -> 346 bytes
 .../test_messages/password_encrypted_foo.asc       |   6 +
 .../test_messages/password_encrypted_foo.gpg       |   1 +
 .../minifi_rs_playground/minifi_rs_playground.md   |   1 +
 .../src/processors/asciify_german/tests.rs         |   7 +-
 .../src/processors/put_file/unix_permissions.rs    |  11 +-
 minifi_rust/minifi_native/src/api/processor.rs     |   3 +-
 .../flow_file_stream_transform.rs                  |   6 +-
 minifi_rust/minifi_native/src/lib.rs               |   2 +
 minifi_rust/minifi_native/src/test_utils.rs        |  16 +
 minifi_rust/minifi_rs_behave/Dockerfile.alpine     |   2 +-
 60 files changed, 3253 insertions(+), 41 deletions(-)

diff --git a/.github/references/ubuntu_22_04_clang_arm_manifest.json b/.github/references/ubuntu_22_04_clang_arm_manifest.json
index 139b3854d..efc67a971 100644
--- a/.github/references/ubuntu_22_04_clang_arm_manifest.json
+++ b/.github/references/ubuntu_22_04_clang_arm_manifest.json
@@ -12104,6 +12104,184 @@
         "version": "1.0.0"
     }
 },
+{
+    "bundles": {
+        "componentManifest": {
+            "processors": [
+                {
+                    "propertyDescriptors": {
+                        "Passphrase": {
+                            "name": "Passphrase",
+                            "description": "Passphrase used for decrypting data encrypted with Password-Based Encryption",
+                            "validator": "NON_BLANK_VALIDATOR",
+                            "required": "false",
+                            "sensitive": "true",
+                            "expressionLanguageScope": "NONE"
+                        },
+                        "Private Key Service": {
+                            "typeProvidedByValue": {
+                                "type": "minifi_pgp.controller_services.private_key_service.PGPPrivateKeyService",
+                                "group": "org.apache.nifi.minifi.rust",
+                                "artifact": "minifi_pgp"
+                            },
+                            "name": "Private Key Service",
+                            "description": "PGP Private Key Service for decrypting data encrypted with Public Key Encryption",
+                            "validator": "VALID",
+                            "required": "false",
+                            "sensitive": "false",
+                            "expressionLanguageScope": "NONE"
+                        }
+                    },
+                    "inputRequirement": "INPUT_REQUIRED",
+                    "isSingleThreaded": "false",
+                    "supportedRelationships": [
+                        {
+                            "name": "failure",
+                            "description": "Decryption Failed"
+                        },
+                        {
+                            "name": "success",
+                            "description": "Decryption Succeeded"
+                        }
+                    ],
+                    "typeDescription": "Decrypt contents of OpenPGP messages.",
+                    "supportsDynamicRelationships": "false",
+                    "supportsDynamicProperties": "false",
+                    "type": "minifi_pgp.processors.decrypt_content.DecryptContentPGP"
+                },
+                {
+                    "propertyDescriptors": {
+                        "File Encoding": {
+                            "name": "File Encoding",
+                            "description": "File Encoding for encryption",
+                            "validator": "VALID",
+                            "required": "true",
+                            "sensitive": "false",
+                            "expressionLanguageScope": "NONE",
+                            "defaultValue": "BINARY",
+                            "allowableValues": [
+                                {
+                                    "value": "ASCII",
+                                    "displayName": "ASCII"
+                                },
+                                {
+                                    "value": "BINARY",
+                                    "displayName": "BINARY"
+                                }
+                            ]
+                        },
+                        "Passphrase": {
+                            "name": "Passphrase",
+                            "description": "Passphrase used for encrypting data with Password-Based Encryption",
+                            "validator": "NON_BLANK_VALIDATOR",
+                            "required": "false",
+                            "sensitive": "true",
+                            "expressionLanguageScope": "NONE"
+                        },
+                        "Public Key Search": {
+                            "name": "Public Key Search",
+                            "description": "PGP Public Key Search will be used to match against the User ID or Key ID when formatted as uppercase hexadecimal string of 16 characters",
+                            "validator": "VALID",
+                            "required": "false",
+                            "sensitive": "false",
+                            "expressionLanguageScope": "FLOWFILE_ATTRIBUTES"
+                        },
+                        "Public Key Service": {
+                            "typeProvidedByValue": {
+                                "type": "minifi_pgp.controller_services.public_key_service.PGPPublicKeyService",
+                                "group": "org.apache.nifi.minifi.rust",
+                                "artifact": "minifi_pgp"
+                            },
+                            "name": "Public Key Service",
+                            "description": "PGP Public Key Service for encrypting data with Public Key Encryption",
+                            "validator": "VALID",
+                            "required": "false",
+                            "sensitive": "false",
+                            "expressionLanguageScope": "NONE"
+                        }
+                    },
+                    "inputRequirement": "INPUT_REQUIRED",
+                    "isSingleThreaded": "false",
+                    "supportedRelationships": [
+                        {
+                            "name": "failure",
+                            "description": "Encryption Failed"
+                        },
+                        {
+                            "name": "success",
+                            "description": "Encryption Succeeded"
+                        }
+                    ],
+                    "typeDescription": "Encrypt contents using OpenPGP.",
+                    "supportsDynamicRelationships": "false",
+                    "supportsDynamicProperties": "false",
+                    "type": "minifi_pgp.processors.encrypt_content.EncryptContentPGP"
+                }
+            ],
+            "controllerServices": [
+                {
+                    "propertyDescriptors": {
+                        "Key Password": {
+                            "name": "Key Password",
+                            "description": "Password used for decrypting Private Keys. Multiple passwords may be supplied one per line, each of them is tried in turn",
+                            "validator": "NON_BLANK_VALIDATOR",
+                            "required": "false",
+                            "sensitive": "true",
+                            "expressionLanguageScope": "NONE"
+                        },
+                        "Keyring": {
+                            "name": "Keyring",
+                            "description": "Secret Key encoded in ASCII Armor",
+                            "validator": "VALID",
+                            "required": "false",
+                            "sensitive": "true",
+                            "expressionLanguageScope": "NONE"
+                        },
+                        "Keyring File": {
+                            "name": "Keyring File",
+                            "description": "File path to PGP Secret Key encoded in binary or ASCII Armor",
+                            "validator": "VALID",
+                            "required": "false",
+                            "sensitive": "false",
+                            "expressionLanguageScope": "FLOWFILE_ATTRIBUTES"
+                        }
+                    },
+                    "typeDescription": "PGP Private Key Service provides Private Keys loaded from files or properties",
+                    "supportsDynamicRelationships": "false",
+                    "supportsDynamicProperties": "false",
+                    "type": "minifi_pgp.controller_services.private_key_service.PGPPrivateKeyService"
+                },
+                {
+                    "propertyDescriptors": {
+                        "Keyring": {
+                            "name": "Keyring",
+                            "description": "PGP Keyring or Public Key encoded in ASCII Armor",
+                            "validator": "VALID",
+                            "required": "false",
+                            "sensitive": "false",
+                            "expressionLanguageScope": "NONE"
+                        },
+                        "Keyring File": {
+                            "name": "Keyring File",
+                            "description": "File path to PGP Keyring or Public Key encoded in binary or ASCII Armor",
+                            "validator": "VALID",
+                            "required": "false",
+                            "sensitive": "false",
+                            "expressionLanguageScope": "FLOWFILE_ATTRIBUTES"
+                        }
+                    },
+                    "typeDescription": "PGP Public Key Service providing Public Keys loaded from files",
+                    "supportsDynamicRelationships": "false",
+                    "supportsDynamicProperties": "false",
+                    "type": "minifi_pgp.controller_services.public_key_service.PGPPublicKeyService"
+                }
+            ]
+        },
+        "group": "org.apache.nifi.minifi.rust",
+        "artifact": "minifi_pgp",
+        "version": "1.0.0"
+    }
+},
 {
     "bundles": {
         "componentManifest": {
diff --git a/CONTROLLERS.md b/CONTROLLERS.md
index 28fce43e1..a888c22a5 100644
--- a/CONTROLLERS.md
+++ b/CONTROLLERS.md
@@ -27,6 +27,8 @@ limitations under the License.
 - [NetworkPrioritizerService](#NetworkPrioritizerService)
 - [ODBCService](#ODBCService)
 - [PersistentMapStateStorage](#PersistentMapStateStorage)
+- [PGPPrivateKeyService](#PGPPrivateKeyService)
+- [PGPPublicKeyService](#PGPPublicKeyService)
 - [ProxyConfigurationService](#ProxyConfigurationService)
 - [RocksDbStateStorage](#RocksDbStateStorage)
 - [SmbConnectionControllerService](#SmbConnectionControllerService)
@@ -245,6 +247,39 @@ In the list below, the names of required properties appear in bold. Any other pr
 | **File**                  |               |                  | Path to a file to store state                                                                                                                          |
 
 
+## PGPPrivateKeyService
+
+### Description
+
+PGP Private Key Service provides Private Keys loaded from files or properties
+
+### Properties
+
+In the list below, the names of required properties appear in bold. Any other properties (not in bold) are considered optional. The table also indicates any default values, and whether a property supports the NiFi Expression Language.
+
+| Name         | Default Value | Allowable Values | Description                                                                                                                                                |
+|--------------|---------------|------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| Key Password |               |                  | Password used for decrypting Private Keys. Multiple passwords may be supplied one per line, each of them is tried in turn<br/>**Sensitive Property: true** |
+| Keyring      |               |                  | Secret Key encoded in ASCII Armor<br/>**Sensitive Property: true**                                                                                         |
+| Keyring File |               |                  | File path to PGP Secret Key encoded in binary or ASCII Armor<br/>**Supports Expression Language: true**                                                    |
+
+
+## PGPPublicKeyService
+
+### Description
+
+PGP Public Key Service providing Public Keys loaded from files
+
+### Properties
+
+In the list below, the names of required properties appear in bold. Any other properties (not in bold) are considered optional. The table also indicates any default values, and whether a property supports the NiFi Expression Language.
+
+| Name         | Default Value | Allowable Values | Description                                                                                                        |
+|--------------|---------------|------------------|--------------------------------------------------------------------------------------------------------------------|
+| Keyring      |               |                  | PGP Keyring or Public Key encoded in ASCII Armor                                                                   |
+| Keyring File |               |                  | File path to PGP Keyring or Public Key encoded in binary or ASCII Armor<br/>**Supports Expression Language: true** |
+
+
 ## ProxyConfigurationService
 
 ### Description
@@ -393,4 +428,3 @@ In the list below, the names of required properties appear in bold. Any other pr
 | **Pretty Print XML**        | false         | true<br/>false                                                        | Specifies whether or not the XML should be pretty printed                                                                                                                                                                                                                                                                                                                            [...]
 | **Name of Record Tag**      |               |                                                                       | Specifies the name of the XML record tag wrapping the record fields.                                                                                                                                                                                                                                                                                                                 [...]
 | **Name of Root Tag**        |               |                                                                       | Specifies the name of the XML root tag wrapping the record set.                                                                                                                                                                                                                                                                                                                      [...]
-
diff --git a/PROCESSORS.md b/PROCESSORS.md
index 4c7705b2a..a2753b42f 100644
--- a/PROCESSORS.md
+++ b/PROCESSORS.md
@@ -26,11 +26,13 @@ limitations under the License.
 - [ConsumeMQTT](#ConsumeMQTT)
 - [ConsumeWindowsEventLog](#ConsumeWindowsEventLog)
 - [ConvertRecord](#ConvertRecord)
+- [DecryptContentPGP](#DecryptContentPGP)
 - [DefragmentText](#DefragmentText)
 - [DeleteAzureBlobStorage](#DeleteAzureBlobStorage)
 - [DeleteAzureDataLakeStorage](#DeleteAzureDataLakeStorage)
 - [DeleteGCSObject](#DeleteGCSObject)
 - [DeleteS3Object](#DeleteS3Object)
+- [EncryptContentPGP](#EncryptContentPGP)
 - [EvaluateJsonPath](#EvaluateJsonPath)
 - [ExecuteProcess](#ExecuteProcess)
 - [ExecuteScript](#ExecuteScript)
@@ -436,6 +438,29 @@ In the list below, the names of required properties appear in bold. Any other pr
 | record.error.message | failure      | This attribute provides on failure the error message encountered by the Reader or Writer. |
 
 
+## DecryptContentPGP
+
+### Description
+
+Decrypt contents of OpenPGP messages.
+
+### Properties
+
+In the list below, the names of required properties appear in bold. Any other properties (not in bold) are considered optional. The table also indicates any default values, and whether a property supports the NiFi Expression Language.
+
+| Name                | Default Value | Allowable Values | Description                                                                                                   |
+|---------------------|---------------|------------------|---------------------------------------------------------------------------------------------------------------|
+| Passphrase          |               |                  | Passphrase used for decrypting data encrypted with Password-Based Encryption<br/>**Sensitive Property: true** |
+| Private Key Service |               |                  | PGP Private Key Service for decrypting data encrypted with Public Key Encryption                              |
+
+### Relationships
+
+| Name    | Description          |
+|---------|----------------------|
+| failure | Decryption Failed    |
+| success | Decryption Succeeded |
+
+
 ## DefragmentText
 
 ### Description
@@ -594,6 +619,37 @@ In the list below, the names of required properties appear in bold. Any other pr
 | failure | FlowFiles are routed to failure relationship |
 
 
+## EncryptContentPGP
+
+### Description
+
+Encrypt contents using OpenPGP.
+
+### Properties
+
+In the list below, the names of required properties appear in bold. Any other properties (not in bold) are considered optional. The table also indicates any default values, and whether a property supports the NiFi Expression Language.
+
+| Name               | Default Value | Allowable Values | Description                                                                                                                                                                          |
+|--------------------|---------------|------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| **File Encoding**  | BINARY        | ASCII<br/>BINARY | File Encoding for encryption                                                                                                                                                         |
+| Passphrase         |               |                  | Passphrase used for encrypting data with Password-Based Encryption<br/>**Sensitive Property: true**                                                                                  |
+| Public Key Search  |               |                  | PGP Public Key Search will be used to match against the User ID or Key ID when formatted as uppercase hexadecimal string of 16 characters<br/>**Supports Expression Language: true** |
+| Public Key Service |               |                  | PGP Public Key Service for encrypting data with Public Key Encryption                                                                                                                |
+
+### Relationships
+
+| Name    | Description          |
+|---------|----------------------|
+| failure | Encryption Failed    |
+| success | Encryption Succeeded |
+
+### Output Attributes
+
+| Attribute         | Relationship | Description   |
+|-------------------|--------------|---------------|
+| pgp.file.encoding | success      | File Encoding |
+
+
 ## EvaluateJsonPath
 
 ### Description
diff --git a/README.md b/README.md
index a26a995c5..b7a1b959e 100644
--- a/README.md
+++ b/README.md
@@ -75,32 +75,33 @@ The following table lists the base set of processors.
 
 The next table outlines CMAKE flags that correspond with MiNiFi extensions. Extensions that are enabled by default ( such as RocksDB ), can be disabled with the respective CMAKE flag on the command line.
 
-| Extension Set                    | Processors and Controller Services                                                                                                                                                                                                                                                                                                                                                                                                                                      [...]
-|----------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- [...]
-| Archive Extensions               | [ApplyTemplate](PROCESSORS.md#applytemplate)<br/>[CompressContent](PROCESSORS.md#compresscontent)<br/>[ManipulateArchive](PROCESSORS.md#manipulatearchive)<br/>[MergeContent](PROCESSORS.md#mergecontent)<br/>[FocusArchiveEntry](PROCESSORS.md#focusarchiveentry)<br/>[UnfocusArchiveEntry](PROCESSORS.md#unfocusarchiveentry)                                                                                                                                         [...]
-| AWS                              | [AWSCredentialsService](CONTROLLERS.md#awscredentialsservice)<br/>[PutS3Object](PROCESSORS.md#puts3object)<br/>[DeleteS3Object](PROCESSORS.md#deletes3object)<br/>[FetchS3Object](PROCESSORS.md#fetchs3object)<br/>[ListS3](PROCESSORS.md#lists3)<br/>[PutKinesisStream](PROCESSORS.md#putkinesisstream)                                                                                                                                                                [...]
-| Azure                            | [AzureStorageCredentialsService](CONTROLLERS.md#azurestoragecredentialsservice)<br/>[PutAzureBlobStorage](PROCESSORS.md#putazureblobstorage)<br/>[DeleteAzureBlobStorage](PROCESSORS.md#deleteazureblobstorage)<br/>[FetchAzureBlobStorage](PROCESSORS.md#fetchazureblobstorage)<br/>[ListAzureBlobStorage](PROCESSORS.md#listazureblobstorage)<br/>[PutAzureDataLakeStorage](PROCESSORS.md#putazuredatalakestorage)<br/>[DeleteAzureDataLakeStorage](PROCESSORS.md#del [...]
-| CivetWeb                         | [ListenHTTP](PROCESSORS.md#listenhttp)                                                                                                                                                                                                                                                                                                                                                                                                                                  [...]
-| Couchbase                        | [CouchbaseClusterService](CONTROLLERS.md#couchbaseclusterservice)<br/>[PutCouchbaseKey](PROCESSORS.md#putcouchbasekey)<br/>[GetCouchbaseKey](PROCESSORS.md#getcouchbasekey)                                                                                                                                                                                                                                                                                             [...]
-| Elasticsearch                    | [ElasticsearchCredentialsControllerService](CONTROLLERS.md#elasticsearchcredentialscontrollerservice)<br/>[PostElasticsearch](PROCESSORS.md#postelasticsearch)                                                                                                                                                                                                                                                                                                          [...]
-| ExecuteProcess (Linux and macOS) | [ExecuteProcess](PROCESSORS.md#executeprocess)                                                                                                                                                                                                                                                                                                                                                                                                                          [...]
-| Google Cloud Platform            | [DeleteGCSObject](PROCESSORS.md#deletegcsobject)<br>[FetchGCSObject](PROCESSORS.md#fetchgcsobject)<br>[GCPCredentialsControllerService](CONTROLLERS.md#gcpcredentialscontrollerservice)<br>[ListGCSBucket](PROCESSORS.md#listgcsbucket)<br>[PutGCSObject](PROCESSORS.md#putgcsobject)                                                                                                                                                                                   [...]
-| Grafana Loki                     | [PushGrafanaLokiREST](PROCESSORS.md#pushgrafanalokirest)<br>[PushGrafanaLokiGrpc](PROCESSORS.md#pushgrafanalokigrpc)                                                                                                                                                                                                                                                                                                                                                    [...]
-| Kafka                            | [PublishKafka](PROCESSORS.md#publishkafka)<br>[ConsumeKafka](PROCESSORS.md#consumekafka)                                                                                                                                                                                                                                                                                                                                                                                [...]
-| Kubernetes (Linux)               | [KubernetesControllerService](CONTROLLERS.md#kubernetescontrollerservice)                                                                                                                                                                                                                                                                                                                                                                                               [...]
-| LlamaCpp                         | [RunLlamaCppInference](PROCESSORS.md#runllamacppinference)                                                                                                                                                                                                                                                                                                                                                                                                              [...]
-| Lua Scripting                    | [ExecuteScript](PROCESSORS.md#executescript)                                                                                                                                                                                                                                                                                                                                                                                                                            [...]
-| MQTT                             | [ConsumeMQTT](PROCESSORS.md#consumemqtt)<br/>[PublishMQTT](PROCESSORS.md#publishmqtt)                                                                                                                                                                                                                                                                                                                                                                                   [...]
-| OPC                              | [FetchOPCProcessor](PROCESSORS.md#fetchopcprocessor)<br/>[PutOPCProcessor](PROCESSORS.md#putopcprocessor)                                                                                                                                                                                                                                                                                                                                                               [...]
-| PDH (Windows)                    | [PerformanceDataMonitor](PROCESSORS.md#performancedatamonitor)                                                                                                                                                                                                                                                                                                                                                                                                          [...]
-| ProcFs (Linux)                   | [ProcFsMonitor](PROCESSORS.md#procfsmonitor)                                                                                                                                                                                                                                                                                                                                                                                                                            [...]
-| Python Scripting                 | [ExecuteScript](PROCESSORS.md#executescript)<br/>[**Custom Python Processors**](extensions/python/PYTHON.md)                                                                                                                                                                                                                                                                                                                                                            [...]
-| SMB (Windows)                    | [FetchSmb](PROCESSORS.md#fetchsmb)<br/>[ListSmb](PROCESSORS.md#listsmb)<br/>[PutSmb](PROCESSORS.md#putsmb)                                                                                                                                                                                                                                                                                                                                                              [...]
-| SFTP                             | [FetchSFTP](PROCESSORS.md#fetchsftp)<br/>[ListSFTP](PROCESSORS.md#listsftp)<br/>[PutSFTP](PROCESSORS.md#putsftp)                                                                                                                                                                                                                                                                                                                                                        [...]
-| SQL                              | [ExecuteSQL](PROCESSORS.md#executesql)<br/>[PutSQL](PROCESSORS.md#putsql)<br/>[QueryDatabaseTable](PROCESSORS.md#querydatabasetable)<br/>                                                                                                                                                                                                                                                                                                                               [...]
-| Splunk                           | [PutSplunkHTTP](PROCESSORS.md#putsplunkhttp)<br/>[QuerySplunkIndexingStatus](PROCESSORS.md#querysplunkindexingstatus)                                                                                                                                                                                                                                                                                                                                                   [...]
-| Systemd (Linux)                  | [ConsumeJournald](PROCESSORS.md#consumejournald)                                                                                                                                                                                                                                                                                                                                                                                                                        [...]
-| Windows Event Log (Windows)      | [ConsumeWindowsEventLog](PROCESSORS.md#consumewindowseventlog)<br/>[TailEventLog](PROCESSORS.md#taileventlog)                                                                                                                                                                                                                                                                                                                                                           [...]
+| Extension Set                    | Processors and Controller Services                                                                                                                                                                                                                                                                                                                                                                                                                                      [...]
+|----------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- [...]
+| Archive Extensions               | [ApplyTemplate](PROCESSORS.md#applytemplate)<br/>[CompressContent](PROCESSORS.md#compresscontent)<br/>[ManipulateArchive](PROCESSORS.md#manipulatearchive)<br/>[MergeContent](PROCESSORS.md#mergecontent)<br/>[FocusArchiveEntry](PROCESSORS.md#focusarchiveentry)<br/>[UnfocusArchiveEntry](PROCESSORS.md#unfocusarchiveentry)                                                                                                                                         [...]
+| AWS                              | [AWSCredentialsService](CONTROLLERS.md#awscredentialsservice)<br/>[PutS3Object](PROCESSORS.md#puts3object)<br/>[DeleteS3Object](PROCESSORS.md#deletes3object)<br/>[FetchS3Object](PROCESSORS.md#fetchs3object)<br/>[ListS3](PROCESSORS.md#lists3)<br/>[PutKinesisStream](PROCESSORS.md#putkinesisstream)                                                                                                                                                                [...]
+| Azure                            | [AzureStorageCredentialsService](CONTROLLERS.md#azurestoragecredentialsservice)<br/>[PutAzureBlobStorage](PROCESSORS.md#putazureblobstorage)<br/>[DeleteAzureBlobStorage](PROCESSORS.md#deleteazureblobstorage)<br/>[FetchAzureBlobStorage](PROCESSORS.md#fetchazureblobstorage)<br/>[ListAzureBlobStorage](PROCESSORS.md#listazureblobstorage)<br/>[PutAzureDataLakeStorage](PROCESSORS.md#putazuredatalakestorage)<br/>[DeleteAzureDataLakeStorage](PROCESSORS.md#del [...]
+| CivetWeb                         | [ListenHTTP](PROCESSORS.md#listenhttp)                                                                                                                                                                                                                                                                                                                                                                                                                                  [...]
+| Couchbase                        | [CouchbaseClusterService](CONTROLLERS.md#couchbaseclusterservice)<br/>[PutCouchbaseKey](PROCESSORS.md#putcouchbasekey)<br/>[GetCouchbaseKey](PROCESSORS.md#getcouchbasekey)                                                                                                                                                                                                                                                                                             [...]
+| Elasticsearch                    | [ElasticsearchCredentialsControllerService](CONTROLLERS.md#elasticsearchcredentialscontrollerservice)<br/>[PostElasticsearch](PROCESSORS.md#postelasticsearch)                                                                                                                                                                                                                                                                                                          [...]
+| ExecuteProcess (Linux and macOS) | [ExecuteProcess](PROCESSORS.md#executeprocess)                                                                                                                                                                                                                                                                                                                                                                                                                          [...]
+| Google Cloud Platform            | [DeleteGCSObject](PROCESSORS.md#deletegcsobject)<br>[FetchGCSObject](PROCESSORS.md#fetchgcsobject)<br>[GCPCredentialsControllerService](CONTROLLERS.md#gcpcredentialscontrollerservice)<br>[ListGCSBucket](PROCESSORS.md#listgcsbucket)<br>[PutGCSObject](PROCESSORS.md#putgcsobject)                                                                                                                                                                                   [...]
+| Grafana Loki                     | [PushGrafanaLokiREST](PROCESSORS.md#pushgrafanalokirest)<br>[PushGrafanaLokiGrpc](PROCESSORS.md#pushgrafanalokigrpc)                                                                                                                                                                                                                                                                                                                                                    [...]
+| Kafka                            | [PublishKafka](PROCESSORS.md#publishkafka)<br>[ConsumeKafka](PROCESSORS.md#consumekafka)                                                                                                                                                                                                                                                                                                                                                                                [...]
+| Kubernetes (Linux)               | [KubernetesControllerService](CONTROLLERS.md#kubernetescontrollerservice)                                                                                                                                                                                                                                                                                                                                                                                               [...]
+| LlamaCpp                         | [RunLlamaCppInference](PROCESSORS.md#runllamacppinference)                                                                                                                                                                                                                                                                                                                                                                                                              [...]
+| Lua Scripting                    | [ExecuteScript](PROCESSORS.md#executescript)                                                                                                                                                                                                                                                                                                                                                                                                                            [...]
+| MQTT                             | [ConsumeMQTT](PROCESSORS.md#consumemqtt)<br/>[PublishMQTT](PROCESSORS.md#publishmqtt)                                                                                                                                                                                                                                                                                                                                                                                   [...]
+| OPC                              | [FetchOPCProcessor](PROCESSORS.md#fetchopcprocessor)<br/>[PutOPCProcessor](PROCESSORS.md#putopcprocessor)                                                                                                                                                                                                                                                                                                                                                               [...]
+| PDH (Windows)                    | [PerformanceDataMonitor](PROCESSORS.md#performancedatamonitor)                                                                                                                                                                                                                                                                                                                                                                                                          [...]
+| PGP                              | [EncryptContentPGP](PROCESSORS.md#encryptcontentpgp)<br/>[DecryptContentPGP](PROCESSORS.md#decryptcontentpgp)<br/>[PGPPublicKeyService](CONTROLLERS.md#pgppublickeyservice)<br/>[PGPPrivateKeyService](CONTROLLERS.md#pgpprivatekeyservice)                                                                                                                                                                                                                             [...]
+| ProcFs (Linux)                   | [ProcFsMonitor](PROCESSORS.md#procfsmonitor)                                                                                                                                                                                                                                                                                                                                                                                                                            [...]
+| Python Scripting                 | [ExecuteScript](PROCESSORS.md#executescript)<br/>[**Custom Python Processors**](extensions/python/PYTHON.md)                                                                                                                                                                                                                                                                                                                                                            [...]
+| SMB (Windows)                    | [FetchSmb](PROCESSORS.md#fetchsmb)<br/>[ListSmb](PROCESSORS.md#listsmb)<br/>[PutSmb](PROCESSORS.md#putsmb)                                                                                                                                                                                                                                                                                                                                                              [...]
+| SFTP                             | [FetchSFTP](PROCESSORS.md#fetchsftp)<br/>[ListSFTP](PROCESSORS.md#listsftp)<br/>[PutSFTP](PROCESSORS.md#putsftp)                                                                                                                                                                                                                                                                                                                                                        [...]
+| SQL                              | [ExecuteSQL](PROCESSORS.md#executesql)<br/>[PutSQL](PROCESSORS.md#putsql)<br/>[QueryDatabaseTable](PROCESSORS.md#querydatabasetable)<br/>                                                                                                                                                                                                                                                                                                                               [...]
+| Splunk                           | [PutSplunkHTTP](PROCESSORS.md#putsplunkhttp)<br/>[QuerySplunkIndexingStatus](PROCESSORS.md#querysplunkindexingstatus)                                                                                                                                                                                                                                                                                                                                                   [...]
+| Systemd (Linux)                  | [ConsumeJournald](PROCESSORS.md#consumejournald)                                                                                                                                                                                                                                                                                                                                                                                                                        [...]
+| Windows Event Log (Windows)      | [ConsumeWindowsEventLog](PROCESSORS.md#consumewindowseventlog)<br/>[TailEventLog](PROCESSORS.md#taileventlog)                                                                                                                                                                                                                                                                                                                                                           [...]
 
  Please see our [Python guide](extensions/python/PYTHON.md) on how to write Python processors and use them within MiNiFi C++.
 
diff --git a/cmake/MiNiFiOptions.cmake b/cmake/MiNiFiOptions.cmake
index 7a898a765..dcd3a2b26 100644
--- a/cmake/MiNiFiOptions.cmake
+++ b/cmake/MiNiFiOptions.cmake
@@ -120,6 +120,7 @@ add_minifi_option(ENABLE_CONTROLLER "Enables the build of MiNiFi controller bina
 add_minifi_option(ENABLE_LLAMACPP "Enables llama.cpp support." ON)
 add_minifi_option(ENABLE_OPC "Instructs the build system to enable the OPC extension" ON)
 add_minifi_option(MINIFI_RUST "Enables the build of rust based extensions." OFF)
+add_minifi_dependent_option(MINIFI_EXTENSION_PGP "Enables the PGP rust extension." ON "MINIFI_RUST" OFF)
 add_minifi_option(MINIFI_LMDB "Enables the LMDB extension." OFF)
 
 set_minifi_cache_variable(CUSTOM_MALLOC OFF "Overwrite malloc implementation.")
diff --git a/minifi_rust/CMakeLists.txt b/minifi_rust/CMakeLists.txt
index 63b8a407e..a9f604011 100644
--- a/minifi_rust/CMakeLists.txt
+++ b/minifi_rust/CMakeLists.txt
@@ -39,8 +39,18 @@ if (NOT ENABLE_TEST_PROCESSORS)
     set_target_properties(cargo-build_minifi_rs_playground PROPERTIES EXCLUDE_FROM_ALL TRUE)
 endif()
 
+if (NOT MINIFI_EXTENSION_PGP)
+    set_target_properties(cargo-build_minifi_pgp PROPERTIES EXCLUDE_FROM_ALL TRUE)
+endif()
+
 include(CTest)
 
+# Build the test binaries during the build
+add_custom_target(cargo_build_tests ALL
+        COMMAND cargo test --no-run
+        WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
+        COMMENT "Building Rust test binaries")
+
 add_test(
         NAME cargo_tests
         COMMAND cargo test
diff --git a/minifi_rust/Cargo.toml b/minifi_rust/Cargo.toml
index 6dc306f11..8bde74c44 100644
--- a/minifi_rust/Cargo.toml
+++ b/minifi_rust/Cargo.toml
@@ -7,3 +7,9 @@ panic = "abort"
 
 [profile.dev]
 panic = "abort"
+
+# The PGP tests unlock passphrase-protected secret keys, which costs a full S2K hash
+# chain per key. Unoptimized that dominates the test suite (6.4s -> 0.4s with this).
+# Workspace members are not matched by "*", so our own code stays at opt-level 0.
+[profile.dev.package."*"]
+opt-level = 2
diff --git a/minifi_rust/extensions/minifi_pgp/.cargo/config.toml b/minifi_rust/extensions/minifi_pgp/.cargo/config.toml
new file mode 100644
index 000000000..cb8c02ddc
--- /dev/null
+++ b/minifi_rust/extensions/minifi_pgp/.cargo/config.toml
@@ -0,0 +1,5 @@
+[target.aarch64-apple-darwin]
+rustflags = ["-C", "link-arg=-undefined", "-C", "link-arg=dynamic_lookup"]
+
+[target.x86_64-apple-darwin]
+rustflags = ["-C", "link-arg=-undefined", "-C", "link-arg=dynamic_lookup"]
diff --git a/minifi_rust/extensions/minifi_pgp/.gitignore b/minifi_rust/extensions/minifi_pgp/.gitignore
new file mode 100644
index 000000000..f9f6d205f
--- /dev/null
+++ b/minifi_rust/extensions/minifi_pgp/.gitignore
@@ -0,0 +1,7 @@
+target
+output
+features/.venv
+features/output
+integration_tests/features/.venv
+integration_tests/features/linux_so
+integration_tests/.venv
\ No newline at end of file
diff --git a/minifi_rust/extensions/minifi_pgp/Cargo.toml b/minifi_rust/extensions/minifi_pgp/Cargo.toml
new file mode 100644
index 000000000..6edd4d3b7
--- /dev/null
+++ b/minifi_rust/extensions/minifi_pgp/Cargo.toml
@@ -0,0 +1,14 @@
+[package]
+name = "minifi_pgp"
+version = "1.0.0"
+edition = "2024"
+
+[lib]
+crate-type = ["cdylib"]
+
+[dependencies]
+minifi_native = { path = "../../minifi_native" }
+strum_macros = "0.28.0"
+strum = "0.28.0"
+pgp = "0.20.0"
+rand = "0.8.6" # pgp 0.20.0 doesnt support >= 0.9 rand yet
diff --git a/minifi_rust/extensions/minifi_pgp/features/encrypt_decrypt.feature b/minifi_rust/extensions/minifi_pgp/features/encrypt_decrypt.feature
new file mode 100644
index 000000000..202a01307
--- /dev/null
+++ b/minifi_rust/extensions/minifi_pgp/features/encrypt_decrypt.feature
@@ -0,0 +1,67 @@
+# 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.
+
+@SUPPORTS_WINDOWS
+Feature: Test PGP extension's encryption and decryption capabilities
+
+  Scenario: The pgp library is loaded into minifi
+    Given log property "logger.org::apache::nifi::minifi::core::extension::ExtensionManager" is set to "TRACE,stderr"
+    And log property "logger.org::apache::nifi::minifi::core::ClassLoader" is set to "TRACE,stderr"
+
+    When the MiNiFi instance starts up
+
+    Then the Minifi logs contain the following message: "Registering class 'EncryptContentPGP' at '/minifi_pgp'" in less than 10 seconds
+    And the Minifi logs contain the following message: "Registering class 'DecryptContentPGP' at '/minifi_pgp'" in less than 1 seconds
+    And the Minifi logs contain the following message: "Registering class 'PGPPublicKeyService' at '/minifi_pgp'" in less than 1 seconds
+    And the Minifi logs contain the following message: "Registering class 'PGPPrivateKeyService' at '/minifi_pgp'" in less than 1 seconds
+    And the Minifi logs do not contain errors
+    And the Minifi logs do not contain warnings
+
+  Scenario: Encrypted for Alice but not for Bob
+    Given log property "logger.minifi_pgp::processors::decrypt_content::DecryptContentPGP" is set to "TRACE,stderr"
+    And log property "logger.minifi_pgp::processors::encrypt_content::EncryptContentPGP" is set to "TRACE,stderr"
+
+    And a GetFile processor with the "Input Directory" property set to "/tmp/input"
+    And an EncryptContentPGP processor with a PGPPublicKeyService is set up
+    And a DecryptContentPGP processor named DecryptAlice with a PGPPrivateKeyService is set up for Alice
+    And a DecryptContentPGP processor named DecryptBob with a PGPPrivateKeyService is set up for Bob
+    And a PutFile processor with the name "AliceSuccess"
+    And a PutFile processor with the name "BobFailure"
+
+    And these processor properties are set
+      | processor name    | property name     | property value       |
+      | EncryptContentPGP | File Encoding     | ASCII                |
+      | EncryptContentPGP | Public Key Search | Alice                |
+      | AliceSuccess      | Directory         | /tmp/output/alice_ok |
+      | BobFailure        | Directory         | /tmp/output/bob_fail |
+
+    And the processors are connected up as described here
+      | source name       | relationship name | destination name  |
+      | GetFile           | success           | EncryptContentPGP |
+      | EncryptContentPGP | success           | DecryptAlice      |
+      | EncryptContentPGP | success           | DecryptBob        |
+      | DecryptAlice      | success           | AliceSuccess      |
+      | DecryptBob        | failure           | BobFailure        |
+
+    And AliceSuccess's success relationship is auto-terminated
+    And BobFailure's success relationship is auto-terminated
+
+    And a directory at "/tmp/input" has a file "test_file.log" with the content "test content"
+
+    When the MiNiFi instance starts up
+
+    Then at least one file with the content "test content" is placed in the "/tmp/output/alice_ok" directory in less than 5 seconds
+    And an encrypted armored pgp file is placed in the "/tmp/output/bob_fail" directory in less than 5 seconds
+    And the Minifi logs do not contain errors
diff --git a/minifi_rust/extensions/minifi_pgp/features/environment.py b/minifi_rust/extensions/minifi_pgp/features/environment.py
new file mode 100644
index 000000000..4652c748a
--- /dev/null
+++ b/minifi_rust/extensions/minifi_pgp/features/environment.py
@@ -0,0 +1,41 @@
+# 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
+#
+#   https://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.
+
+import os
+from pathlib import Path
+
+from minifi_behave.core.hooks import (
+    add_extension_to_minifi_container,
+    common_after_scenario,
+    common_before_scenario,
+)
+
+
+def before_all(context):
+    dir_path = os.path.dirname(os.path.realpath(__file__))
+    build_path = os.path.normpath(os.path.join(dir_path, "../../../target/release/"))
+    add_extension_to_minifi_container("minifi_pgp", [build_path], context)
+
+
+def before_scenario(context, scenario):
+    context.minifi_container_image = "apacheminificpp:minifi_pgp"
+    common_before_scenario(context, scenario)
+    context.resource_dir = Path(f"{os.path.dirname(os.path.realpath(__file__))}/..")
+
+
+def after_scenario(context, scenario):
+    common_after_scenario(context, scenario)
diff --git a/minifi_rust/extensions/minifi_pgp/features/steps/steps.py b/minifi_rust/extensions/minifi_pgp/features/steps/steps.py
new file mode 100644
index 000000000..fd5699553
--- /dev/null
+++ b/minifi_rust/extensions/minifi_pgp/features/steps/steps.py
@@ -0,0 +1,80 @@
+# 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
+#
+#   https://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.
+
+
+import humanfriendly
+from behave import step, then
+from minifi_behave.core.helpers import wait_for_condition
+from minifi_behave.core.minifi_test_context import MinifiTestContext
+from minifi_behave.minifi.controller_service import ControllerService
+from minifi_behave.minifi.processor import Processor
+from minifi_behave.steps import (
+    checking_steps,  # noqa: F401
+    configuration_steps,  # noqa: F401
+    core_steps,  # noqa: F401
+    flow_building_steps,  # noqa: F401
+)
+
+
+@step("an EncryptContentPGP processor with a PGPPublicKeyService is set up")
+def step_encrypt_content_with_service(context: MinifiTestContext):
+    public_key_service = ControllerService(class_name="PGPPublicKeyService", service_name="my_public_keys")
+    alice_public_key = (context.resource_dir / "test_keys" / "keyring.asc").read_text()
+    public_key_service.add_property("Keyring", alice_public_key)
+    context.get_or_create_default_minifi_container().flow_definition.controller_services.append(public_key_service)
+
+    processor = Processor("EncryptContentPGP", "EncryptContentPGP")
+    processor.add_property("Public Key Service", "my_public_keys")
+    context.get_or_create_default_minifi_container().flow_definition.processors.append(processor)
+
+
+@step("a DecryptContentPGP processor named DecryptAlice with a PGPPrivateKeyService is set up for Alice")
+def step_decrypt_content_for_alice(context: MinifiTestContext):
+    private_key_service = ControllerService(class_name="PGPPrivateKeyService", service_name="alice_private_key")
+    alice_private_key = (context.resource_dir / "test_keys" / "alice_private.asc").read_text()
+    private_key_service.add_property("Keyring", alice_private_key)
+    private_key_service.add_property("Key Password", "whiterabbit")
+    context.get_or_create_default_minifi_container().flow_definition.controller_services.append(private_key_service)
+
+    processor = Processor("DecryptContentPGP", "DecryptAlice")
+    processor.add_property("Private Key Service", "alice_private_key")
+    context.get_or_create_default_minifi_container().flow_definition.processors.append(processor)
+
+
+@step("a DecryptContentPGP processor named DecryptBob with a PGPPrivateKeyService is set up for Bob")
+def step_decrypt_content_for_bob(context: MinifiTestContext):
+    private_key_service = ControllerService(class_name="PGPPrivateKeyService", service_name="bob_private_key")
+    bob_private_key = (context.resource_dir / "test_keys" / "bob_private.asc").read_text()
+    private_key_service.add_property("Keyring", bob_private_key)
+    context.get_or_create_default_minifi_container().flow_definition.controller_services.append(private_key_service)
+
+    processor = Processor("DecryptContentPGP", "DecryptBob")
+    processor.add_property("Private Key Service", "bob_private_key")
+    context.get_or_create_default_minifi_container().flow_definition.processors.append(processor)
+
+
+@then('an encrypted armored pgp file is placed in the "{directory}" directory in less than {duration}')
+def then_armored_pgp_file_in_dir(context: MinifiTestContext, directory: str, duration: str):
+    duration_seconds = humanfriendly.parse_timespan(duration)
+    assert wait_for_condition(
+        condition=lambda: context.get_or_create_default_minifi_container().directory_contains_file_with_regex(
+            directory, "-----BEGIN PGP MESSAGE-----"
+        ),
+        timeout_seconds=duration_seconds,
+        bail_condition=lambda: False,
+        context=context,
+    )
diff --git a/minifi_rust/extensions/minifi_pgp/minifi_pgp.md b/minifi_rust/extensions/minifi_pgp/minifi_pgp.md
new file mode 100644
index 000000000..e72bc0b3f
--- /dev/null
+++ b/minifi_rust/extensions/minifi_pgp/minifi_pgp.md
@@ -0,0 +1,112 @@
+<!--
+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.
+-->
+
+## Table of Contents
+
+### Processors
+
+- [DecryptContentPGP](#DecryptContentPGP)
+- [EncryptContentPGP](#EncryptContentPGP)
+### Controller Services
+
+- [PGPPrivateKeyService](#PGPPrivateKeyService)
+- [PGPPublicKeyService](#PGPPublicKeyService)
+
+
+## DecryptContentPGP
+
+### Description
+
+Decrypt contents of OpenPGP messages.
+
+### Properties
+
+In the list below, the names of required properties appear in bold. Any other properties (not in bold) are considered optional. The table also indicates any default values, and whether a property supports the NiFi Expression Language.
+
+| Name                | Default Value | Allowable Values | Description                                                                                                   |
+|---------------------|---------------|------------------|---------------------------------------------------------------------------------------------------------------|
+| Passphrase          |               |                  | Passphrase used for decrypting data encrypted with Password-Based Encryption<br/>**Sensitive Property: true** |
+| Private Key Service |               |                  | PGP Private Key Service for decrypting data encrypted with Public Key Encryption                              |
+
+### Relationships
+
+| Name    | Description          |
+|---------|----------------------|
+| failure | Decryption Failed    |
+| success | Decryption Succeeded |
+
+
+## EncryptContentPGP
+
+### Description
+
+Encrypt contents using OpenPGP.
+
+### Properties
+
+In the list below, the names of required properties appear in bold. Any other properties (not in bold) are considered optional. The table also indicates any default values, and whether a property supports the NiFi Expression Language.
+
+| Name               | Default Value | Allowable Values | Description                                                                                                                                                                          |
+|--------------------|---------------|------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| **File Encoding**  | BINARY        | ASCII<br/>BINARY | File Encoding for encryption                                                                                                                                                         |
+| Passphrase         |               |                  | Passphrase used for encrypting data with Password-Based Encryption<br/>**Sensitive Property: true**                                                                                  |
+| Public Key Search  |               |                  | PGP Public Key Search will be used to match against the User ID or Key ID when formatted as uppercase hexadecimal string of 16 characters<br/>**Supports Expression Language: true** |
+| Public Key Service |               |                  | PGP Public Key Service for encrypting data with Public Key Encryption                                                                                                                |
+
+### Relationships
+
+| Name    | Description          |
+|---------|----------------------|
+| failure | Encryption Failed    |
+| success | Encryption Succeeded |
+
+### Output Attributes
+
+| Attribute         | Relationship | Description   |
+|-------------------|--------------|---------------|
+| pgp.file.encoding | success      | File Encoding |
+
+
+## PGPPrivateKeyService
+
+### Description
+
+PGP Private Key Service provides Private Keys loaded from files or properties
+
+### Properties
+
+In the list below, the names of required properties appear in bold. Any other properties (not in bold) are considered optional. The table also indicates any default values, and whether a property supports the NiFi Expression Language.
+
+| Name         | Default Value | Allowable Values | Description                                                                                                                                                |
+|--------------|---------------|------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| Key Password |               |                  | Password used for decrypting Private Keys. Multiple passwords may be supplied one per line, each of them is tried in turn<br/>**Sensitive Property: true** |
+| Keyring      |               |                  | Secret Key encoded in ASCII Armor<br/>**Sensitive Property: true**                                                                                         |
+| Keyring File |               |                  | File path to PGP Secret Key encoded in binary or ASCII Armor<br/>**Supports Expression Language: true**                                                    |
+
+
+## PGPPublicKeyService
+
+### Description
+
+PGP Public Key Service providing Public Keys loaded from files
+
+### Properties
+
+In the list below, the names of required properties appear in bold. Any other properties (not in bold) are considered optional. The table also indicates any default values, and whether a property supports the NiFi Expression Language.
+
+| Name         | Default Value | Allowable Values | Description                                                                                                        |
+|--------------|---------------|------------------|--------------------------------------------------------------------------------------------------------------------|
+| Keyring      |               |                  | PGP Keyring or Public Key encoded in ASCII Armor                                                                   |
+| Keyring File |               |                  | File path to PGP Keyring or Public Key encoded in binary or ASCII Armor<br/>**Supports Expression Language: true** |
diff --git a/minifi_rust/extensions/minifi_pgp/src/controller_services/encryption_key.rs b/minifi_rust/extensions/minifi_pgp/src/controller_services/encryption_key.rs
new file mode 100644
index 000000000..173f7d436
--- /dev/null
+++ b/minifi_rust/extensions/minifi_pgp/src/controller_services/encryption_key.rs
@@ -0,0 +1,131 @@
+// 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
+//
+//   https://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.
+
+use minifi_native::MinifiError;
+use pgp::composed::{SignedPublicKey, SignedPublicSubKey};
+use pgp::packet::{PublicKey, SignatureType};
+use pgp::types::KeyDetails;
+
+/// The component key of a certificate that a message should actually be encrypted to.
+///
+/// `SignedPublicKey`'s own `EncryptionKey` implementation always uses the primary key and
+/// ignores subkeys, which fails for the layout `gpg --gen-key` produces nowadays: a sign-only
+/// primary key (Ed25519) plus a dedicated encryption subkey (Cv25519).
+#[derive(Debug)]
+pub(crate) enum EncryptionTarget<'a> {
+    Primary(&'a PublicKey),
+    Subkey(&'a SignedPublicSubKey),
+}
+
+/// Picks the key of `certificate` to encrypt to.
+///
+/// Encryption subkeys are preferred, newest first, the way GnuPG picks them; the primary key is
+/// only used when the certificate has no usable encryption subkey.
+pub(crate) fn select_encryption_target(
+    certificate: &SignedPublicKey,
+) -> Result<EncryptionTarget<'_>, MinifiError> {
+    let newest_encryption_subkey = certificate
+        .public_subkeys
+        .iter()
+        .filter(|subkey| is_encryption_subkey(subkey))
+        .max_by_key(|subkey| subkey.created_at());
+
+    if let Some(subkey) = newest_encryption_subkey {
+        return Ok(EncryptionTarget::Subkey(subkey));
+    }
+
+    if certificate.primary_key.algorithm().can_encrypt() {
+        return Ok(EncryptionTarget::Primary(&certificate.primary_key));
+    }
+
+    Err(MinifiError::custom(format!(
+        "Key {} cannot be used for encryption, it has no encryption subkey and its primary key is {:?} which cannot encrypt",
+        certificate.primary_key.fingerprint(),
+        certificate.primary_key.algorithm()
+    )))
+}
+
+fn is_encryption_subkey(subkey: &SignedPublicSubKey) -> bool {
+    if !subkey.key.algorithm().can_encrypt() {
+        return false;
+    }
+
+    let is_revoked = subkey
+        .signatures
+        .iter()
+        .any(|signature| signature.typ() == Some(SignatureType::SubkeyRevocation));
+    if is_revoked {
+        return false;
+    }
+
+    subkey
+        .signatures
+        .iter()
+        .filter(|signature| signature.typ() == Some(SignatureType::SubkeyBinding))
+        .any(|signature| {
+            let key_flags = signature.key_flags();
+            key_flags.encrypt_comms() || key_flags.encrypt_storage()
+        })
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::test_utils::get_test_key_path;
+    use pgp::composed::Deserializable;
+
+    fn load_certificate(file_name: &str) -> SignedPublicKey {
+        let (certificate, _headers) =
+            SignedPublicKey::from_armor_file(get_test_key_path(file_name)).unwrap();
+        certificate
+    }
+
+    #[test]
+    fn rsa_primary_key_is_used_when_there_is_no_encryption_subkey() {
+        // alice.asc is an RSA key whose primary key carries the encrypt capability itself.
+        let certificate = load_certificate("alice.asc");
+        assert!(certificate.primary_key.algorithm().can_encrypt());
+        assert!(matches!(
+            select_encryption_target(&certificate).unwrap(),
+            EncryptionTarget::Primary(_)
+        ));
+    }
+
+    #[test]
+    fn encryption_subkey_is_preferred_over_a_sign_only_primary_key() {
+        // dave.asc has an Ed25519 sign-only primary key and a Cv25519 encryption subkey,
+        // the layout `gpg --gen-key` produces by default.
+        let certificate = load_certificate("dave.asc");
+        assert!(!certificate.primary_key.algorithm().can_encrypt());
+
+        let target = select_encryption_target(&certificate).unwrap();
+        let EncryptionTarget::Subkey(subkey) = target else {
+            panic!("expected the encryption subkey to be selected");
+        };
+        assert!(subkey.key.algorithm().can_encrypt());
+    }
+
+    #[test]
+    fn sign_only_key_without_encryption_subkey_is_rejected() {
+        // erin.asc is an Ed25519 sign-only primary key with no subkeys at all.
+        let certificate = load_certificate("erin.asc");
+        let err = select_encryption_target(&certificate)
+            .unwrap_err()
+            .to_string();
+        assert!(err.contains("cannot be used for encryption"), "{err}");
+    }
+}
diff --git a/minifi_rust/extensions/minifi_pgp/src/controller_services/key_file_property.rs b/minifi_rust/extensions/minifi_pgp/src/controller_services/key_file_property.rs
new file mode 100644
index 000000000..d367d404f
--- /dev/null
+++ b/minifi_rust/extensions/minifi_pgp/src/controller_services/key_file_property.rs
@@ -0,0 +1,118 @@
+// 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
+//
+//   https://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.
+
+use crate::controller_services::key_parsing::{KeyKind, parse_key_file};
+use minifi_native::{MinifiError, PropertyConstraints, PropertySchema, PropertyType};
+use pgp::composed::{SignedPublicKey, SignedSecretKey};
+
+pub(crate) struct SecretKeyFile {}
+
+impl PropertySchema for SecretKeyFile {
+    const CONSTRAINT: Option<PropertyConstraints> = None;
+    const IS_REQUIRED: bool = true;
+}
+
+impl PropertyType for SecretKeyFile {
+    type Output = Vec<SignedSecretKey>;
+
+    fn parse(s: &str) -> Result<Self::Output, MinifiError> {
+        parse_key_file(s, KeyKind::Secret)
+    }
+}
+
+pub(crate) struct PublicKeyFile {}
+impl PropertySchema for PublicKeyFile {
+    const CONSTRAINT: Option<PropertyConstraints> = None;
+    const IS_REQUIRED: bool = true;
+}
+
+impl PropertyType for PublicKeyFile {
+    type Output = Vec<SignedPublicKey>;
+
+    fn parse(s: &str) -> Result<Self::Output, MinifiError> {
+        parse_key_file(s, KeyKind::Public)
+    }
+}
+
+#[cfg(test)]
+mod secret_key_file_tests {
+    use super::*;
+    use crate::test_utils::get_test_key_path;
+
+    fn assert_invalid_secret_key_file(file_name: &str) {
+        assert!(SecretKeyFile::parse(&get_test_key_path(file_name)).is_err())
+    }
+    fn assert_valid_secret_key_file(file_name: &str) {
+        assert!(
+            !SecretKeyFile::parse(&get_test_key_path(file_name))
+                .unwrap()
+                .is_empty()
+        )
+    }
+    #[test]
+    fn test_invalid_secret_keyfiles() {
+        assert_invalid_secret_key_file("alice.asc");
+        assert_invalid_secret_key_file("alice.gpg");
+        assert_invalid_secret_key_file("garbage.gpg");
+        assert_invalid_secret_key_file("truncated_private.asc");
+        assert_invalid_secret_key_file("non_existent.asc");
+    }
+
+    #[test]
+    fn test_valid_secret_keyfiles() {
+        assert_valid_secret_key_file("alice_private.asc");
+        assert_valid_secret_key_file("alice_private.gpg");
+        assert_valid_secret_key_file("bob_private.asc");
+        assert_valid_secret_key_file("bob_private.gpg");
+        assert_valid_secret_key_file("secret_keyring.asc");
+        assert_valid_secret_key_file("secret_keyring.gpg");
+    }
+}
+
+#[cfg(test)]
+mod public_key_file_tests {
+    use crate::controller_services::key_file_property::PublicKeyFile;
+    use crate::test_utils::get_test_key_path;
+    use minifi_native::PropertyType;
+
+    fn assert_invalid_public_key_file(file_name: &str) {
+        assert!(PublicKeyFile::parse(&get_test_key_path(file_name)).is_err())
+    }
+    fn assert_valid_public_key_file(file_name: &str) {
+        assert!(
+            !PublicKeyFile::parse(&get_test_key_path(file_name))
+                .unwrap()
+                .is_empty()
+        )
+    }
+    #[test]
+    fn test_invalid_public_keyfiles() {
+        assert_invalid_public_key_file("alice_private.asc");
+        assert_invalid_public_key_file("alice_private.gpg");
+        assert_invalid_public_key_file("garbage.gpg");
+        assert_invalid_public_key_file("truncated.asc");
+        assert_invalid_public_key_file("non_existent.asc");
+    }
+
+    #[test]
+    fn test_valid_public_keyfiles() {
+        assert_valid_public_key_file("alice.asc");
+        assert_valid_public_key_file("alice.gpg");
+        assert_valid_public_key_file("keyring.asc");
+        assert_valid_public_key_file("keyring.gpg");
+    }
+}
diff --git a/minifi_rust/extensions/minifi_pgp/src/controller_services/key_lookup.rs b/minifi_rust/extensions/minifi_pgp/src/controller_services/key_lookup.rs
new file mode 100644
index 000000000..26481bac9
--- /dev/null
+++ b/minifi_rust/extensions/minifi_pgp/src/controller_services/key_lookup.rs
@@ -0,0 +1,141 @@
+// 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
+//
+//   https://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.
+
+use minifi_native::MinifiError;
+use pgp::composed::SignedKeyDetails;
+use pgp::types::KeyId;
+
+fn matches_key_id(key_id: &KeyId, target: &str) -> bool {
+    key_id.to_string().eq_ignore_ascii_case(target)
+}
+
+fn matches_user_id(details: &SignedKeyDetails, target_lower: &str) -> bool {
+    details.users.iter().any(|user| {
+        user.id
+            .as_str()
+            .map(|user_id| user_id.to_ascii_lowercase().contains(target_lower))
+            .unwrap_or(false)
+    })
+}
+
+pub(crate) fn find_unique_key<'a, K, F>(
+    keys: &'a [K],
+    target_id: &str,
+    key_parts: F,
+) -> Result<&'a K, MinifiError>
+where
+    F: Fn(&'a K) -> (KeyId, &'a SignedKeyDetails),
+{
+    let target = target_id.trim();
+    if target.is_empty() {
+        return Err(MinifiError::custom("No key search string was given"));
+    }
+
+    if let Some(key) = keys
+        .iter()
+        .find(|key| matches_key_id(&key_parts(key).0, target))
+    {
+        return Ok(key);
+    }
+
+    let target_lower = target.to_ascii_lowercase();
+    let mut matches = keys
+        .iter()
+        .filter(|key| matches_user_id(key_parts(key).1, &target_lower));
+
+    let Some(first_match) = matches.next() else {
+        return Err(MinifiError::custom(format!(
+            "No key matching '{target}' was found"
+        )));
+    };
+
+    let ambiguous: Vec<String> = std::iter::once(first_match)
+        .chain(matches)
+        .map(|key| key_parts(key).0.to_string())
+        .collect();
+    if ambiguous.len() > 1 {
+        return Err(MinifiError::custom(format!(
+            "'{target}' is ambiguous, it matches {} keys: {}",
+            ambiguous.len(),
+            ambiguous.join(", ")
+        )));
+    }
+
+    Ok(first_match)
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use pgp::composed::SignedKeyDetails;
+
+    /// A stand-in for a key: just the parts `find_unique_key` looks at.
+    #[derive(Debug)]
+    struct TestKey {
+        key_id: KeyId,
+        details: SignedKeyDetails,
+    }
+
+    fn key_id_from_hex(hex: &str) -> KeyId {
+        let mut bytes = [0u8; 8];
+        for (i, chunk) in hex.as_bytes().chunks(2).take(8).enumerate() {
+            bytes[i] = u8::from_str_radix(std::str::from_utf8(chunk).unwrap(), 16).unwrap();
+        }
+        KeyId::from(bytes)
+    }
+
+    fn find<'a>(keys: &'a [TestKey], target: &str) -> Result<&'a TestKey, MinifiError> {
+        find_unique_key(keys, target, |key| (key.key_id, &key.details))
+    }
+
+    fn no_details() -> SignedKeyDetails {
+        SignedKeyDetails::new(vec![], vec![], vec![], vec![])
+    }
+
+    #[test]
+    fn empty_target_never_matches() {
+        let keys = [TestKey {
+            key_id: key_id_from_hex("1122334455667788"),
+            details: no_details(),
+        }];
+        assert!(find(&keys, "").is_err());
+        assert!(find(&keys, "   ").is_err());
+    }
+
+    #[test]
+    fn matches_key_id_case_insensitive() {
+        let keys = [TestKey {
+            key_id: key_id_from_hex("11ABcdEF33445566"),
+            details: no_details(),
+        }];
+
+        assert!(find(&keys, "11abcdef33445566").is_ok());
+        assert!(find(&keys, "11ABCDEF33445566").is_ok());
+        assert!(find(&keys, "11abcdef3344556").is_err()); // 15 chars
+        assert!(find(&keys, "abcdef33445566").is_err());
+    }
+
+    #[test]
+    fn missing_key_reports_the_search_string() {
+        let keys = [TestKey {
+            key_id: key_id_from_hex("1122334455667788"),
+            details: no_details(),
+        }];
+        let err = find(&keys, "99aabbccddeeff00").unwrap_err().to_string();
+        assert!(err.contains("99aabbccddeeff00"), "{err}");
+    }
+}
diff --git a/minifi_rust/extensions/minifi_pgp/src/controller_services/key_parsing.rs b/minifi_rust/extensions/minifi_pgp/src/controller_services/key_parsing.rs
new file mode 100644
index 000000000..70acdcb27
--- /dev/null
+++ b/minifi_rust/extensions/minifi_pgp/src/controller_services/key_parsing.rs
@@ -0,0 +1,90 @@
+// 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
+//
+//   https://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.
+
+use minifi_native::{GetProperty, MinifiError, Property, PropertyType};
+use pgp::composed::Deserializable;
+
+/// Describes which flavour of key is being loaded, used for error messages only.
+#[derive(Copy, Clone)]
+pub(crate) enum KeyKind {
+    Public,
+    Secret,
+}
+
+impl KeyKind {
+    fn no_valid_keys(self) -> MinifiError {
+        match self {
+            KeyKind::Public => MinifiError::validation("Couldn't load any valid public keys"),
+            KeyKind::Secret => MinifiError::validation("Couldn't load any valid secret keys"),
+        }
+    }
+}
+
+/// Parses every key found in ASCII Armored `input`.
+pub(crate) fn parse_armored_keys<T: Deserializable>(
+    input: &str,
+    kind: KeyKind,
+) -> Result<Vec<T>, MinifiError> {
+    let mut keys: Vec<T> = Vec::new();
+    if let Ok((parsed, _headers)) = T::from_armor_many(input.as_bytes()) {
+        keys.extend(parsed.filter_map(Result::ok));
+    }
+    non_empty(keys, kind)
+}
+
+/// Parses every key found in the file at `path`, which may be ASCII Armored or binary.
+pub(crate) fn parse_key_file<T: Deserializable>(
+    path: &str,
+    kind: KeyKind,
+) -> Result<Vec<T>, MinifiError> {
+    let mut keys: Vec<T> = Vec::new();
+    if let Ok((parsed, _headers)) = T::from_armor_file_many(path) {
+        keys.extend(parsed.filter_map(Result::ok));
+    } else if let Ok(parsed) = T::from_file_many(path) {
+        keys.extend(parsed.filter_map(Result::ok));
+    }
+    non_empty(keys, kind)
+}
+
+/// Loads the keys of a controller service from its file property and its inline property,
+/// failing when neither yields a usable key.
+pub(crate) fn load_service_keys<T, File, Inline, Ctx>(
+    context: &Ctx,
+    file_property: &Property<Option<File>>,
+    inline_property: &Property<Option<Inline>>,
+) -> Result<Vec<T>, MinifiError>
+where
+    Ctx: GetProperty,
+    File: PropertyType<Output = Vec<T>>,
+    Inline: PropertyType<Output = Vec<T>>,
+{
+    let mut keys = context.get_property(file_property)?.unwrap_or_default();
+    keys.extend(context.get_property(inline_property)?.unwrap_or_default());
+
+    if keys.is_empty() {
+        return Err(MinifiError::validation("Could not load any valid keys"));
+    }
+    Ok(keys)
+}
+
+fn non_empty<T>(keys: Vec<T>, kind: KeyKind) -> Result<Vec<T>, MinifiError> {
+    if keys.is_empty() {
+        Err(kind.no_valid_keys())
+    } else {
+        Ok(keys)
+    }
+}
diff --git a/minifi_rust/extensions/minifi_pgp/src/controller_services/key_property.rs b/minifi_rust/extensions/minifi_pgp/src/controller_services/key_property.rs
new file mode 100644
index 000000000..fc5c629a2
--- /dev/null
+++ b/minifi_rust/extensions/minifi_pgp/src/controller_services/key_property.rs
@@ -0,0 +1,49 @@
+// 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
+//
+//   https://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.
+
+use crate::controller_services::key_parsing::{KeyKind, parse_armored_keys};
+use minifi_native::{MinifiError, PropertyConstraints, PropertySchema, PropertyType};
+use pgp::composed::{SignedPublicKey, SignedSecretKey};
+
+pub(crate) struct SecretKey {}
+
+impl PropertySchema for SecretKey {
+    const CONSTRAINT: Option<PropertyConstraints> = None;
+    const IS_REQUIRED: bool = true;
+}
+
+impl PropertyType for SecretKey {
+    type Output = Vec<SignedSecretKey>;
+
+    fn parse(s: &str) -> Result<Self::Output, MinifiError> {
+        parse_armored_keys(s, KeyKind::Secret)
+    }
+}
+
+pub(crate) struct PublicKey {}
+impl PropertySchema for PublicKey {
+    const CONSTRAINT: Option<PropertyConstraints> = None;
+    const IS_REQUIRED: bool = true;
+}
+
+impl PropertyType for PublicKey {
+    type Output = Vec<SignedPublicKey>;
+
+    fn parse(s: &str) -> Result<Self::Output, MinifiError> {
+        parse_armored_keys(s, KeyKind::Public)
+    }
+}
diff --git a/minifi_rust/extensions/minifi_pgp/src/controller_services/mod.rs b/minifi_rust/extensions/minifi_pgp/src/controller_services/mod.rs
new file mode 100644
index 000000000..3aa314bc0
--- /dev/null
+++ b/minifi_rust/extensions/minifi_pgp/src/controller_services/mod.rs
@@ -0,0 +1,24 @@
+// 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
+//
+//   https://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.
+
+pub(crate) mod encryption_key;
+mod key_file_property;
+mod key_lookup;
+mod key_parsing;
+mod key_property;
+pub(crate) mod private_key_service;
+pub(crate) mod public_key_service;
diff --git a/minifi_rust/extensions/minifi_pgp/src/controller_services/private_key_service.rs b/minifi_rust/extensions/minifi_pgp/src/controller_services/private_key_service.rs
new file mode 100644
index 000000000..56171434e
--- /dev/null
+++ b/minifi_rust/extensions/minifi_pgp/src/controller_services/private_key_service.rs
@@ -0,0 +1,249 @@
+// 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
+//
+//   https://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.
+
+#[cfg(test)]
+use crate::controller_services::key_lookup::find_unique_key;
+use crate::controller_services::key_parsing::load_service_keys;
+use minifi_native::macros::ComponentIdentifier;
+use minifi_native::{EnableControllerService, GetProperty, Logger, MinifiError};
+use pgp::composed::{SignedSecretKey, TheRing};
+#[cfg(test)]
+use pgp::types::KeyDetails;
+use service_def::*;
+
+#[derive(Debug, ComponentIdentifier)]
+pub(crate) struct PGPPrivateKeyService {
+    private_keys: Vec<SignedSecretKey>,
+    passphrases: Vec<pgp::types::Password>,
+}
+
+impl EnableControllerService for PGPPrivateKeyService {
+    fn enable<P: GetProperty, L: Logger>(context: &P, _logger: &L) -> Result<Self, MinifiError>
+    where
+        Self: Sized,
+    {
+        let private_keys = load_service_keys(context, &KEY_FILE, &KEY)?;
+        let passphrases = context.get_property(&KEY_PASSWORD)?.unwrap_or_default();
+
+        Ok(Self {
+            private_keys,
+            passphrases,
+        })
+    }
+}
+
+impl PGPPrivateKeyService {
+    pub fn get_the_ring(&'_ self) -> TheRing<'_> {
+        TheRing {
+            secret_keys: self.private_keys.iter().collect(),
+            key_passwords: self.passphrases.iter().collect(),
+            message_password: vec![],
+            session_keys: vec![],
+            decrypt_options: Default::default(),
+        }
+    }
+
+    #[cfg(test)]
+    pub fn get_secret_key(&self, target_id: &str) -> Result<&SignedSecretKey, MinifiError> {
+        find_unique_key(&self.private_keys, target_id, |private_key| {
+            (
+                private_key.primary_key.legacy_key_id(),
+                &private_key.details,
+            )
+        })
+    }
+}
+
+mod service_def {
+    use crate::controller_services::key_file_property::SecretKeyFile;
+    use crate::controller_services::key_property::SecretKey;
+    use crate::controller_services::private_key_service::PGPPrivateKeyService;
+    use crate::utils;
+    use minifi_native::{
+        ControllerServiceDefinition, Property, PropertyDefinition, ProvidedInterface,
+        property_definitions,
+    };
+
+    pub(super) const KEY_FILE: Property<Option<SecretKeyFile>> = Property::new(
+        "Keyring File",
+        "File path to PGP Secret Key encoded in binary or ASCII Armor",
+    )
+    .supports_expression_language();
+
+    pub(super) const KEY: Property<Option<SecretKey>> =
+        Property::new("Keyring", "Secret Key encoded in ASCII Armor").sensitive();
+
+    pub(super) const KEY_PASSWORD: Property<Option<utils::Passwords>> = Property::new(
+        "Key Password",
+        "Password used for decrypting Private Keys. Multiple passwords may be supplied one per line, each of them is tried in turn",
+    )
+    .sensitive();
+
+    impl ControllerServiceDefinition for PGPPrivateKeyService {
+        const DESCRIPTION: &'static str =
+            "PGP Private Key Service provides Private Keys loaded from files or properties";
+        const PROPERTIES: &'static [PropertyDefinition] =
+            property_definitions![KEY_FILE, KEY, KEY_PASSWORD];
+        const PROVIDED_APIS: &'static [ProvidedInterface<Self>] = &[];
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::test_utils::get_test_key_path;
+    use minifi_native::{ComponentIdentifier, MockControllerServiceContext, MockLogger};
+
+    #[test]
+    fn test_component_id() {
+        assert_eq!(
+            PGPPrivateKeyService::CLASS_NAME,
+            "minifi_pgp::controller_services::private_key_service::PGPPrivateKeyService"
+        );
+        assert_eq!(PGPPrivateKeyService::GROUP_NAME, "minifi_pgp");
+        assert_eq!(PGPPrivateKeyService::VERSION, "1.0.0");
+    }
+
+    #[test]
+    fn default_fails() {
+        let context = MockControllerServiceContext::new();
+        assert!(PGPPrivateKeyService::enable(&context, &MockLogger::new()).is_err());
+    }
+
+    #[test]
+    fn single_armored_key_file() {
+        let mut context = MockControllerServiceContext::new();
+        context.properties.insert(
+            "Keyring File".to_string(),
+            get_test_key_path("alice_private.asc"),
+        );
+
+        let service =
+            PGPPrivateKeyService::enable(&context, &MockLogger::new()).expect("should enable");
+        assert!(service.get_secret_key("Alice").is_ok());
+        assert!(service.get_secret_key("alice@example.com").is_ok());
+
+        assert!(service.get_secret_key("Bob").is_err());
+        assert!(service.get_secret_key("Carol").is_err());
+    }
+
+    #[test]
+    fn single_binary_key_file() {
+        let mut context = MockControllerServiceContext::new();
+        context.properties.insert(
+            "Keyring File".to_string(),
+            get_test_key_path("alice_private.gpg"),
+        );
+
+        let service =
+            PGPPrivateKeyService::enable(&context, &MockLogger::new()).expect("should enable");
+        assert!(service.get_secret_key("A").is_ok());
+        assert!(service.get_secret_key("Alice").is_ok());
+        assert!(service.get_secret_key("Alice <al...@example.com>").is_ok());
+
+        assert!(service.get_secret_key("<Alice>").is_err());
+
+        assert!(service.get_secret_key("Bob").is_err());
+        assert!(service.get_secret_key("Carol").is_err());
+    }
+
+    #[test]
+    fn armored_keyring_key_file() {
+        let mut context = MockControllerServiceContext::new();
+        context.properties.insert(
+            "Keyring File".to_string(),
+            get_test_key_path("secret_keyring.asc"),
+        );
+
+        let service =
+            PGPPrivateKeyService::enable(&context, &MockLogger::new()).expect("should enable");
+        assert!(service.get_secret_key("Alice").is_ok());
+        assert!(service.get_secret_key("Bob").is_ok());
+        assert!(service.get_secret_key("bob@home.io").is_ok());
+        assert!(service.get_secret_key("bob@work.com").is_ok());
+        assert!(service.get_secret_key("Carol").is_err());
+    }
+
+    #[test]
+    fn binary_keyring_key_file() {
+        let mut context = MockControllerServiceContext::new();
+        context.properties.insert(
+            "Keyring File".to_string(),
+            get_test_key_path("secret_keyring.gpg"),
+        );
+
+        let service =
+            PGPPrivateKeyService::enable(&context, &MockLogger::new()).expect("should enable");
+        assert!(service.get_secret_key("Alice").is_ok());
+        assert!(service.get_secret_key("Bob").is_ok());
+        assert!(service.get_secret_key("bob@home.io").is_ok());
+        assert!(service.get_secret_key("bob@work.com").is_ok());
+        assert!(service.get_secret_key("Carol").is_err());
+    }
+
+    #[test]
+    fn armored_keyring() {
+        let mut context = MockControllerServiceContext::new();
+
+        let file_content = std::fs::read_to_string(get_test_key_path("secret_keyring.asc"))
+            .expect("required for test");
+
+        context
+            .properties
+            .insert("Keyring".to_string(), file_content);
+
+        let service =
+            PGPPrivateKeyService::enable(&context, &MockLogger::new()).expect("should enable");
+        assert!(service.get_secret_key("Alice").is_ok());
+        assert!(service.get_secret_key("Bob").is_ok());
+        assert!(service.get_secret_key("bob@home.io").is_ok());
+        assert!(service.get_secret_key("bob@work.com").is_ok());
+        assert!(service.get_secret_key("Carol").is_err());
+    }
+
+    #[test]
+    fn armored_single_key() {
+        let mut context = MockControllerServiceContext::new();
+
+        let file_content = std::fs::read_to_string(get_test_key_path("alice_private.asc"))
+            .expect("required for test");
+
+        context
+            .properties
+            .insert("Keyring".to_string(), file_content);
+
+        let service =
+            PGPPrivateKeyService::enable(&context, &MockLogger::new()).expect("should enable");
+        assert!(service.get_secret_key("Alice").is_ok());
+        assert!(service.get_secret_key("Bob").is_err());
+        assert!(service.get_secret_key("Carol").is_err());
+    }
+
+    #[test]
+    fn public_ascii_key() {
+        let mut context = MockControllerServiceContext::new();
+
+        let file_content =
+            std::fs::read_to_string(get_test_key_path("alice.asc")).expect("required for test");
+
+        context
+            .properties
+            .insert("Keyring".to_string(), file_content);
+
+        assert!(PGPPrivateKeyService::enable(&context, &MockLogger::new()).is_err());
+    }
+}
diff --git a/minifi_rust/extensions/minifi_pgp/src/controller_services/public_key_service.rs b/minifi_rust/extensions/minifi_pgp/src/controller_services/public_key_service.rs
new file mode 100644
index 000000000..1fc0987c5
--- /dev/null
+++ b/minifi_rust/extensions/minifi_pgp/src/controller_services/public_key_service.rs
@@ -0,0 +1,296 @@
+// 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
+//
+//   https://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.
+
+use crate::controller_services::key_lookup::find_unique_key;
+use crate::controller_services::key_parsing::load_service_keys;
+use minifi_native::macros::ComponentIdentifier;
+use minifi_native::{EnableControllerService, GetProperty, Logger, MinifiError};
+use pgp::composed::SignedPublicKey;
+use pgp::types::KeyDetails;
+use service_def::*;
+
+#[derive(Debug, ComponentIdentifier, PartialEq)]
+pub(crate) struct PGPPublicKeyService {
+    public_keys: Vec<SignedPublicKey>,
+}
+
+impl EnableControllerService for PGPPublicKeyService {
+    fn enable<P: GetProperty, L: Logger>(context: &P, _logger: &L) -> Result<Self, MinifiError>
+    where
+        Self: Sized,
+    {
+        let public_keys = load_service_keys(context, &KEYRING_FILE, &KEYRING)?;
+        Ok(Self { public_keys })
+    }
+}
+
+impl PGPPublicKeyService {
+    pub fn get(&self, target_id: &str) -> Result<&SignedPublicKey, MinifiError> {
+        find_unique_key(&self.public_keys, target_id, |public_key| {
+            (public_key.primary_key.legacy_key_id(), &public_key.details)
+        })
+    }
+}
+
+mod service_def {
+    use crate::controller_services::key_file_property::PublicKeyFile;
+    use crate::controller_services::key_property::PublicKey;
+    use crate::controller_services::public_key_service::PGPPublicKeyService;
+    use minifi_native::{
+        ControllerServiceDefinition, Property, PropertyDefinition, ProvidedInterface,
+        property_definitions,
+    };
+
+    pub(crate) const KEYRING_FILE: Property<Option<PublicKeyFile>> = Property::new(
+        "Keyring File",
+        "File path to PGP Keyring or Public Key encoded in binary or ASCII Armor",
+    )
+    .supports_expression_language();
+
+    pub(crate) const KEYRING: Property<Option<PublicKey>> = Property::new(
+        "Keyring",
+        "PGP Keyring or Public Key encoded in ASCII Armor",
+    );
+
+    impl ControllerServiceDefinition for PGPPublicKeyService {
+        const DESCRIPTION: &'static str =
+            "PGP Public Key Service providing Public Keys loaded from files";
+        const PROPERTIES: &'static [PropertyDefinition] =
+            property_definitions![KEYRING_FILE, KEYRING];
+        const PROVIDED_APIS: &'static [ProvidedInterface<Self>] = &[];
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::test_utils::get_test_key_path;
+
+    use minifi_native::{ComponentIdentifier, MockControllerServiceContext, MockLogger};
+
+    #[test]
+    fn test_component_id() {
+        assert_eq!(
+            PGPPublicKeyService::CLASS_NAME,
+            "minifi_pgp::controller_services::public_key_service::PGPPublicKeyService"
+        );
+        assert_eq!(PGPPublicKeyService::GROUP_NAME, "minifi_pgp");
+        assert_eq!(PGPPublicKeyService::VERSION, "1.0.0");
+    }
+
+    #[test]
+    fn default_fails() {
+        let context = MockControllerServiceContext::new();
+        assert!(PGPPublicKeyService::enable(&context, &MockLogger::new()).is_err());
+    }
+
+    #[test]
+    fn armored_private_key_file() {
+        let mut context = MockControllerServiceContext::new();
+        context.properties.insert(
+            "Keyring File".to_string(),
+            get_test_key_path("alice_private.asc"),
+        );
+
+        assert!(PGPPublicKeyService::enable(&context, &MockLogger::new()).is_err());
+    }
+
+    #[test]
+    fn single_armored_key_file() {
+        let mut context = MockControllerServiceContext::new();
+        context
+            .properties
+            .insert("Keyring File".to_string(), get_test_key_path("alice.asc"));
+
+        let controller_service = PGPPublicKeyService::enable(&context, &MockLogger::new())
+            .expect("enable should succeed");
+
+        assert!(controller_service.get("Alice").is_ok());
+        assert!(controller_service.get("alice@example.com").is_ok());
+
+        assert!(controller_service.get("Bob").is_err());
+        assert!(controller_service.get("Carol").is_err());
+    }
+
+    #[test]
+    fn single_binary_key_file() {
+        let mut context = MockControllerServiceContext::new();
+        context
+            .properties
+            .insert("Keyring File".to_string(), get_test_key_path("alice.gpg"));
+
+        let service = PGPPublicKeyService::enable(&context, &MockLogger::new())
+            .expect("enable should succeed");
+        assert!(service.get("A").is_ok());
+        assert!(service.get("Alice").is_ok());
+        assert!(service.get("Alice <al...@example.com>").is_ok());
+
+        assert!(service.get("<Alice>").is_err());
+
+        assert!(service.get("Bob").is_err());
+        assert!(service.get("Carol").is_err());
+    }
+
+    #[test]
+    fn armored_keyring_key_file() {
+        let mut context = MockControllerServiceContext::new();
+        context
+            .properties
+            .insert("Keyring File".to_string(), get_test_key_path("keyring.asc"));
+
+        let service = PGPPublicKeyService::enable(&context, &MockLogger::new())
+            .expect("enable should succeed");
+        assert!(service.get("Alice").is_ok());
+        assert!(service.get("Bob").is_ok());
+        assert!(service.get("bob@home.io").is_ok());
+        assert!(service.get("bob@work.com").is_ok());
+        assert!(service.get("Carol").is_err());
+    }
+
+    #[test]
+    fn binary_keyring_key_file() {
+        let mut context = MockControllerServiceContext::new();
+        context
+            .properties
+            .insert("Keyring File".to_string(), get_test_key_path("keyring.gpg"));
+
+        let service = PGPPublicKeyService::enable(&context, &MockLogger::new())
+            .expect("enable should succeed");
+        assert!(service.get("Alice").is_ok());
+        assert!(service.get("Bob").is_ok());
+        assert!(service.get("bob@home.io").is_ok());
+        assert!(service.get("bob@work.com").is_ok());
+        assert!(service.get("Carol").is_err());
+    }
+
+    #[test]
+    fn armored_keyring() {
+        let mut context = MockControllerServiceContext::new();
+
+        let file_content =
+            std::fs::read_to_string(get_test_key_path("keyring.asc")).expect("required for test");
+
+        context
+            .properties
+            .insert("Keyring".to_string(), file_content);
+
+        let service = PGPPublicKeyService::enable(&context, &MockLogger::new())
+            .expect("enable should succeed");
+        assert!(service.get("Alice").is_ok());
+        assert!(service.get("Bob").is_ok());
+        assert!(service.get("bob@home.io").is_ok());
+        assert!(service.get("bob@work.com").is_ok());
+        assert!(service.get("Carol").is_err());
+    }
+
+    #[test]
+    fn armored_single_key() {
+        let mut context = MockControllerServiceContext::new();
+
+        let file_content =
+            std::fs::read_to_string(get_test_key_path("alice.asc")).expect("required for test");
+
+        context
+            .properties
+            .insert("Keyring".to_string(), file_content);
+
+        let service = PGPPublicKeyService::enable(&context, &MockLogger::new())
+            .expect("enable should succeed");
+        assert!(service.get("Alice").is_ok());
+        assert!(service.get("Bob").is_err());
+        assert!(service.get("Carol").is_err());
+    }
+
+    #[test]
+    fn private_ascii_key() {
+        let mut context = MockControllerServiceContext::new();
+
+        let file_content = std::fs::read_to_string(get_test_key_path("alice_private.asc"))
+            .expect("required for test");
+
+        context
+            .properties
+            .insert("Keyring".to_string(), file_content);
+
+        assert!(PGPPublicKeyService::enable(&context, &MockLogger::new()).is_err());
+    }
+
+    #[test]
+    fn looks_up_by_key_id_hex() {
+        let mut context = MockControllerServiceContext::new();
+        context
+            .properties
+            .insert("Keyring File".to_string(), get_test_key_path("alice.asc"));
+
+        let service = PGPPublicKeyService::enable(&context, &MockLogger::new())
+            .expect("enable should succeed");
+
+        let alice = service.get("Alice").expect("Alice should exist");
+        let key_id_hex = alice.primary_key.legacy_key_id().to_string();
+        assert_eq!(key_id_hex.len(), 16);
+        assert!(service.get(&key_id_hex).is_ok());
+        assert!(service.get(&key_id_hex.to_ascii_uppercase()).is_ok());
+        assert!(service.get(&key_id_hex[..8]).is_err());
+        assert!(service.get("0123456789abcdef").is_err());
+    }
+
+    fn ambiguous_keyring_service() -> PGPPublicKeyService {
+        let mut context = MockControllerServiceContext::new();
+        context.properties.insert(
+            "Keyring File".to_string(),
+            get_test_key_path("ambiguous_keyring.gpg"),
+        );
+
+        PGPPublicKeyService::enable(&context, &MockLogger::new()).expect("enable should succeed")
+    }
+
+    /// A User ID search that matches more than one key must fail loudly instead of picking one
+    /// of them, otherwise a look-alike key silently becomes the recipient.
+    #[test]
+    fn a_user_id_matching_several_keys_is_reported_as_ambiguous() {
+        let service = ambiguous_keyring_service();
+
+        // "bob@home.io" is a substring of the look-alike "bob@home.io.attacker.test" too.
+        let err = service.get("bob@home.io").unwrap_err().to_string();
+        assert!(err.contains("ambiguous"), "{err}");
+        assert!(err.contains("bob@home.io"), "{err}");
+
+        // Unaffected searches still resolve.
+        assert!(service.get("Alice").is_ok());
+        assert!(service.get("bob@work.com").is_ok());
+        assert!(service.get("bob@home.io.attacker.test").is_ok());
+    }
+
+    /// An exact Key ID is never ambiguous, so it stays usable as the way to disambiguate.
+    #[test]
+    fn a_key_id_search_wins_over_an_ambiguous_user_id() {
+        let service = ambiguous_keyring_service();
+
+        let real_bob = service
+            .get("bob@work.com")
+            .expect("the real Bob should be found by his unique User ID");
+        let real_bob_key_id = real_bob.primary_key.legacy_key_id().to_string();
+
+        let found = service
+            .get(&real_bob_key_id)
+            .expect("a Key ID search should never be ambiguous");
+        assert_eq!(
+            found.primary_key.legacy_key_id(),
+            real_bob.primary_key.legacy_key_id()
+        );
+    }
+}
diff --git a/minifi_rust/extensions/minifi_pgp/src/lib.rs b/minifi_rust/extensions/minifi_pgp/src/lib.rs
new file mode 100644
index 000000000..eba818524
--- /dev/null
+++ b/minifi_rust/extensions/minifi_pgp/src/lib.rs
@@ -0,0 +1,41 @@
+// 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
+//
+//   https://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.
+
+mod controller_services;
+mod processors;
+
+use crate::controller_services::private_key_service::PGPPrivateKeyService;
+use crate::controller_services::public_key_service::PGPPublicKeyService;
+use crate::processors::decrypt_content::DecryptContentPGP;
+use crate::processors::encrypt_content::EncryptContentPGP;
+use minifi_native::{FlowFileStreamTransformProcessorType, MultiThreaded};
+
+minifi_native::declare_minifi_extension!(
+    group_name: "org.apache.nifi.minifi.rust",
+    processors: [
+        (FlowFileStreamTransformProcessorType, MultiThreaded, EncryptContentPGP),
+        (FlowFileStreamTransformProcessorType, MultiThreaded, DecryptContentPGP),
+    ],
+    controllers: [
+        PGPPublicKeyService,
+        PGPPrivateKeyService,
+    ]
+);
+
+#[cfg(test)]
+mod test_utils;
+mod utils;
diff --git a/minifi_rust/extensions/minifi_pgp/src/processors/decrypt_content.rs b/minifi_rust/extensions/minifi_pgp/src/processors/decrypt_content.rs
new file mode 100644
index 000000000..f80101410
--- /dev/null
+++ b/minifi_rust/extensions/minifi_pgp/src/processors/decrypt_content.rs
@@ -0,0 +1,474 @@
+// 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
+//
+//   https://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.
+
+use proc_def::*;
+
+use crate::controller_services::private_key_service::PGPPrivateKeyService;
+
+use minifi_native::macros::ComponentIdentifier;
+use minifi_native::{
+    FlowFileStreamTransform, GetControllerService, GetProperty, InputStream, Logger, MinifiError,
+    OutputStream, ProcessError, RouteErrorExt, Schedule, TransformStreamResult,
+};
+use pgp::composed::{Message, TheRing};
+
+#[derive(Debug, ComponentIdentifier)]
+pub(crate) struct DecryptContentPGP {
+    symmetric_password: Option<pgp::types::Password>,
+}
+
+impl Schedule for DecryptContentPGP {
+    fn schedule<P: GetProperty + GetControllerService, L>(
+        context: &P,
+        _logger: &L,
+    ) -> Result<Self, MinifiError>
+    where
+        Self: Sized,
+        L: Logger,
+    {
+        let symmetric_password = context.get_property(&SYMMETRIC_PASSPHRASE)?;
+        let private_key_service = context.get_controller_service(&PRIVATE_KEY_SERVICE)?;
+        if private_key_service.is_none() && symmetric_password.is_none() {
+            Err(MinifiError::validation(
+                "Either Symmetric Password or Private Key Service must be set",
+            ))
+        } else {
+            Ok(DecryptContentPGP { symmetric_password })
+        }
+    }
+}
+
+impl DecryptContentPGP {
+    fn decrypt_msg<'a>(
+        &'a self,
+        msg: Message<'a>,
+        private_key_service: Option<&'a PGPPrivateKeyService>,
+    ) -> pgp::errors::Result<Message<'a>> {
+        let mut ring = if let Some(pks) = private_key_service {
+            pks.get_the_ring()
+        } else {
+            TheRing::default()
+        };
+
+        ring.decrypt_options = ring.decrypt_options.enable_gnupg_aead();
+
+        if let Some(sym_passwd) = &self.symmetric_password {
+            ring.message_password.push(sym_passwd);
+        }
+        let (decrypted_msg, _ring_result) = msg.decrypt_the_ring(ring, false)?;
+        Ok(decrypted_msg)
+    }
+}
+
+impl FlowFileStreamTransform for DecryptContentPGP {
+    fn transform<Ctx: GetProperty + GetControllerService, LoggerImpl: Logger>(
+        &self,
+        context: &Ctx,
+        input_stream: &mut dyn InputStream,
+        output_stream: &mut dyn OutputStream,
+        _logger: &LoggerImpl,
+    ) -> Result<TransformStreamResult, ProcessError> {
+        let private_key_service = context.get_controller_service(&PRIVATE_KEY_SERVICE)?;
+
+        let msg = Message::from_reader(input_stream)
+            .map(|(msg, _header)| msg)
+            .route_err_to_failure()?;
+
+        let mut decrypted_msg = self
+            .decrypt_msg(msg, private_key_service)
+            .route_err_to_failure()?;
+
+        if decrypted_msg.is_compressed() {
+            decrypted_msg = decrypted_msg
+                .decompress()
+                .map_err(MinifiError::other)
+                .route_err_to_failure()?
+        };
+
+        let _written_bytes =
+            std::io::copy(&mut decrypted_msg.into_inner(), output_stream).route_err_to_failure()?;
+
+        Ok(TransformStreamResult::new(&SUCCESS))
+    }
+}
+
+mod proc_def {
+    use super::*;
+    use crate::controller_services::private_key_service::PGPPrivateKeyService;
+    use crate::utils;
+    use minifi_native::{
+        OutputAttribute, ProcessorDefinition, ProcessorInputRequirement, Property,
+        PropertyDefinition, Relationship, property_definitions,
+    };
+
+    pub(super) const SYMMETRIC_PASSPHRASE: Property<Option<utils::Password>> = Property::new(
+        "Passphrase",
+        "Passphrase used for decrypting data encrypted with Password-Based Encryption",
+    )
+    .sensitive();
+
+    pub(super) const PRIVATE_KEY_SERVICE: Property<Option<PGPPrivateKeyService>> = Property::new(
+        "Private Key Service",
+        "PGP Private Key Service for decrypting data encrypted with Public Key Encryption",
+    );
+
+    pub(super) const SUCCESS: Relationship = Relationship {
+        name: "success",
+        description: "Decryption Succeeded",
+    };
+
+    pub(super) const FAILURE: Relationship = Relationship {
+        name: "failure",
+        description: "Decryption Failed",
+    };
+
+    impl ProcessorDefinition for DecryptContentPGP {
+        const DESCRIPTION: &'static str = "Decrypt contents of OpenPGP messages.";
+        const INPUT_REQUIREMENT: ProcessorInputRequirement = ProcessorInputRequirement::Required;
+        const SUPPORTS_DYNAMIC_PROPERTIES: bool = false;
+        const SUPPORTS_DYNAMIC_RELATIONSHIPS: bool = false;
+        const OUTPUT_ATTRIBUTES: &'static [OutputAttribute] = &[];
+        const RELATIONSHIPS: &'static [Relationship] = &[SUCCESS, FAILURE];
+        const PROPERTIES: &[PropertyDefinition] =
+            property_definitions![SYMMETRIC_PASSPHRASE, PRIVATE_KEY_SERVICE,];
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::test_utils;
+    use crate::test_utils::get_test_message;
+    use minifi_native::{
+        ComponentIdentifier, EnableControllerService, IoState, MockControllerServiceContext,
+        MockLogger, MockProcessContext, test,
+    };
+    #[test]
+    fn test_ids() {
+        assert_eq!(
+            DecryptContentPGP::CLASS_NAME,
+            "minifi_pgp::processors::decrypt_content::DecryptContentPGP"
+        );
+        assert_eq!(DecryptContentPGP::GROUP_NAME, "minifi_pgp");
+        assert_eq!(DecryptContentPGP::VERSION, "1.0.0");
+    }
+
+    #[test]
+    fn fails_to_schedule_by_default() {
+        let decrypt_content =
+            DecryptContentPGP::schedule(&MockProcessContext::new(), &MockLogger::new());
+        assert!(decrypt_content.is_err());
+    }
+
+    #[test]
+    fn schedules_with_password() {
+        let mut context = MockProcessContext::new();
+        context.properties.insert(
+            SYMMETRIC_PASSPHRASE.name(),
+            "my_secret_password".to_string(),
+        );
+        let decrypt_content = DecryptContentPGP::schedule(&context, &MockLogger::new());
+        assert!(decrypt_content.is_ok());
+    }
+
+    #[test]
+    fn schedule_fails_with_invalid_controller() {
+        let mut context = MockProcessContext::new();
+        context.properties.insert(
+            PRIVATE_KEY_SERVICE.name(),
+            "invalid_private_key_service".to_string(),
+        );
+        let decrypt_content = DecryptContentPGP::schedule(&context, &MockLogger::new());
+        assert!(decrypt_content.is_err());
+    }
+
+    #[derive(Copy, Clone)]
+    struct PrivateKeyData {
+        key_filename: &'static str,
+        passphrase: Option<&'static str>,
+    }
+
+    impl PrivateKeyData {
+        fn into_controller(self) -> PGPPrivateKeyService {
+            let mut context = MockControllerServiceContext::new();
+            context.properties.insert(
+                "Keyring File",
+                test_utils::get_test_key_path(self.key_filename),
+            );
+
+            if let Some(passphrase) = self.passphrase {
+                context.properties.insert("Key Password", passphrase);
+            }
+
+            PGPPrivateKeyService::enable(&context, &MockLogger::new()).expect("should enable")
+        }
+    }
+
+    fn test_decryption(
+        message_file_name: &str,
+        private_key_data: Option<PrivateKeyData>,
+        symmetric_password: Option<&'static str>,
+        expected_result: Result<&[u8], ()>,
+    ) {
+        let mut processor_context = MockProcessContext::new();
+        if let Some(private_key) = private_key_data {
+            processor_context.controller_services.insert(
+                "my_private_key_service".to_string(),
+                Box::new(private_key.into_controller()),
+            );
+            processor_context.properties.insert(
+                PRIVATE_KEY_SERVICE.name(),
+                "my_private_key_service".to_string(),
+            );
+        }
+        if let Some(symmetric_password) = symmetric_password {
+            processor_context
+                .properties
+                .insert(SYMMETRIC_PASSPHRASE.name(), symmetric_password.to_string());
+        }
+
+        let decrypt_content = DecryptContentPGP::schedule(&processor_context, &MockLogger::new())
+            .expect("Should schedule with the configured properties");
+        let mut output: Vec<u8> = Vec::new();
+        let mut flow_file_stream = std::io::Cursor::new(get_test_message(message_file_name));
+        let res = decrypt_content.transform(
+            &processor_context,
+            &mut flow_file_stream,
+            &mut output,
+            &MockLogger::new(),
+        );
+
+        match expected_result {
+            Ok(result_bytes) => {
+                let res = res.expect("Should be able to transform");
+                assert_eq!(res.target_relationship_name(), SUCCESS.name);
+                assert_eq!(res.write_status(), IoState::Ok);
+                assert_eq!(output, result_bytes);
+            }
+            Err(_) => test::assert_routed_to(res, &FAILURE),
+        }
+    }
+
+    #[test]
+    fn decrypts_with_password() {
+        test_decryption(
+            "password_encrypted_foo.gpg",
+            None,
+            Some("my_secret_password"),
+            Ok("foo\n".as_bytes()),
+        );
+        test_decryption(
+            "password_encrypted_foo.asc",
+            None,
+            Some("my_secret_password"),
+            Ok("foo\n".as_bytes()),
+        );
+        test_decryption(
+            "foo_for_alice.gpg",
+            None,
+            Some("my_secret_password"),
+            Err(()),
+        );
+        test_decryption(
+            "foo_for_alice.asc",
+            None,
+            Some("my_secret_password"),
+            Err(()),
+        );
+    }
+
+    #[test]
+    fn decrypts_for_alice() {
+        let alice_private_key_data = PrivateKeyData {
+            key_filename: "alice_private.asc",
+            passphrase: Some("whiterabbit"),
+        };
+
+        test_decryption(
+            "foo_for_alice.asc",
+            Some(alice_private_key_data),
+            None,
+            Ok("foo\n".as_bytes()),
+        );
+
+        test_decryption(
+            "foo_for_alice.gpg",
+            Some(alice_private_key_data),
+            None,
+            Ok("foo\n".as_bytes()),
+        );
+
+        test_decryption(
+            "password_encrypted_foo.gpg",
+            Some(alice_private_key_data),
+            None,
+            Err(()),
+        );
+
+        test_decryption(
+            "password_encrypted_foo.asc",
+            Some(alice_private_key_data),
+            None,
+            Err(()),
+        );
+    }
+
+    /// Encrypts `content` for `public_key_search` using `keyring_file` and returns the ciphertext.
+    fn encrypt_for(keyring_file: &str, public_key_search: &str, content: &[u8]) -> Vec<u8> {
+        use crate::controller_services::public_key_service::PGPPublicKeyService;
+        use crate::processors::encrypt_content::EncryptContentPGP;
+
+        let mut key_service_context = MockControllerServiceContext::new();
+        key_service_context.properties.insert(
+            "Keyring File".to_string(),
+            test_utils::get_test_key_path(keyring_file),
+        );
+        let key_service = PGPPublicKeyService::enable(&key_service_context, &MockLogger::new())
+            .expect("should enable");
+
+        let mut context = MockProcessContext::new();
+        context.properties.extend([
+            ("Public Key Service", "my_public_key_service"),
+            ("Public Key Search", public_key_search),
+        ]);
+        context
+            .controller_services
+            .insert("my_public_key_service".to_string(), Box::new(key_service));
+
+        let mut ciphertext: Vec<u8> = Vec::new();
+        let mut plaintext = std::io::Cursor::new(content);
+        let encrypt_content =
+            EncryptContentPGP::schedule(&context, &MockLogger::new()).expect("should schedule");
+        encrypt_content
+            .transform(
+                &context,
+                &mut plaintext,
+                &mut ciphertext,
+                &MockLogger::new(),
+            )
+            .expect("should encrypt");
+        ciphertext
+    }
+
+    fn decrypt_with(private_key_data: PrivateKeyData, ciphertext: Vec<u8>) -> Vec<u8> {
+        let mut context = MockProcessContext::new();
+        context.controller_services.insert(
+            "my_private_key_service".to_string(),
+            Box::new(private_key_data.into_controller()),
+        );
+        context.properties.insert(
+            PRIVATE_KEY_SERVICE.name(),
+            "my_private_key_service".to_string(),
+        );
+
+        let decrypt_content =
+            DecryptContentPGP::schedule(&context, &MockLogger::new()).expect("should schedule");
+        let mut output: Vec<u8> = Vec::new();
+        let mut ciphertext = std::io::Cursor::new(ciphertext);
+        let res = decrypt_content
+            .transform(&context, &mut ciphertext, &mut output, &MockLogger::new())
+            .expect("should decrypt");
+        assert_eq!(res.target_relationship_name(), SUCCESS.name);
+        assert_eq!(res.write_status(), IoState::Ok);
+        output
+    }
+
+    /// Dave's primary key is Ed25519, which cannot encrypt; only his Cv25519 subkey can. This
+    /// round trip only works if EncryptContentPGP encrypts to the subkey.
+    #[test]
+    fn round_trip_with_a_sign_only_primary_key() {
+        let ciphertext = encrypt_for("dave.asc", "dave@example.com", b"for dave only");
+
+        let dave_private_key = PrivateKeyData {
+            key_filename: "dave_private.asc",
+            passphrase: Some("gardenparty"),
+        };
+        assert_eq!(decrypt_with(dave_private_key, ciphertext), b"for dave only");
+    }
+
+    /// A keyring holding keys with different passphrases needs every one of those passphrases,
+    /// which "Key Password" accepts one per line.
+    #[test]
+    fn decrypts_with_one_of_several_key_passwords() {
+        let ciphertext = encrypt_for("dave.asc", "dave@example.com", b"for dave only");
+
+        // mixed_secret_keyring.gpg holds Alice (whiterabbit) and Dave (gardenparty).
+        let both_passwords = PrivateKeyData {
+            key_filename: "mixed_secret_keyring.gpg",
+            passphrase: Some("whiterabbit\ngardenparty"),
+        };
+        assert_eq!(
+            decrypt_with(both_passwords, ciphertext.clone()),
+            b"for dave only"
+        );
+
+        // Alice's passphrase alone cannot unlock Dave's key.
+        let alice_password_only = PrivateKeyData {
+            key_filename: "mixed_secret_keyring.gpg",
+            passphrase: Some("whiterabbit"),
+        };
+        let mut context = MockProcessContext::new();
+        context.controller_services.insert(
+            "my_private_key_service".to_string(),
+            Box::new(alice_password_only.into_controller()),
+        );
+        context.properties.insert(
+            PRIVATE_KEY_SERVICE.name(),
+            "my_private_key_service".to_string(),
+        );
+        let decrypt_content =
+            DecryptContentPGP::schedule(&context, &MockLogger::new()).expect("should schedule");
+        let mut output: Vec<u8> = Vec::new();
+        let mut ciphertext = std::io::Cursor::new(ciphertext);
+        let res =
+            decrypt_content.transform(&context, &mut ciphertext, &mut output, &MockLogger::new());
+        test::assert_routed_to(res, &FAILURE);
+    }
+
+    #[test]
+    fn decryption_of_not_encrypted_data() {
+        let alice_private_key = PrivateKeyData {
+            key_filename: "alice_private.asc",
+            passphrase: Some("whiterabbit"),
+        };
+
+        let mut processor_context = MockProcessContext::new();
+        processor_context.controller_services.insert(
+            "my_private_key_service".to_string(),
+            Box::new(alice_private_key.into_controller()),
+        );
+        processor_context.properties.insert(
+            PRIVATE_KEY_SERVICE.name(),
+            "my_private_key_service".to_string(),
+        );
+
+        let logger = MockLogger::new();
+
+        let decrypt_content = DecryptContentPGP::schedule(&processor_context, &logger)
+            .expect("Should schedule without any properties");
+        let mut result: Vec<u8> = vec![];
+        let mut flow_file_stream = std::io::Cursor::new("something not encrypted".as_bytes());
+        let res = decrypt_content.transform(
+            &processor_context,
+            &mut flow_file_stream,
+            &mut result,
+            &logger,
+        );
+
+        test::assert_routed_to(res, &FAILURE);
+    }
+}
diff --git a/minifi_rust/extensions/minifi_pgp/src/processors/encrypt_content.rs b/minifi_rust/extensions/minifi_pgp/src/processors/encrypt_content.rs
new file mode 100644
index 000000000..14b6ce3ca
--- /dev/null
+++ b/minifi_rust/extensions/minifi_pgp/src/processors/encrypt_content.rs
@@ -0,0 +1,392 @@
+// 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
+//
+//   https://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.
+
+use crate::controller_services::encryption_key::{EncryptionTarget, select_encryption_target};
+use minifi_native::{
+    FlowFileStreamTransform, GetAttribute, GetControllerService, GetId, GetProperty, InputStream,
+    Logger, MinifiError, OutputStream, ProcessError, RouteErrorExt, Schedule,
+    TransformStreamResult,
+};
+use pgp::composed::{ArmorOptions, MessageBuilder, SignedPublicKey};
+use pgp::types::{Password, StringToKey};
+
+use proc_def::*;
+
+use minifi_native::macros::{ComponentIdentifier, PropertyType};
+use strum_macros::{Display, EnumString, IntoStaticStr, VariantNames};
+
+#[derive(
+    Debug, Clone, Copy, PartialEq, Display, EnumString, VariantNames, IntoStaticStr, PropertyType,
+)]
+#[strum(serialize_all = "UPPERCASE", const_into_str)]
+enum FileEncoding {
+    Ascii,
+    Binary,
+}
+
+#[derive(Debug, ComponentIdentifier)]
+pub(crate) struct EncryptContentPGP {
+    file_encoding: FileEncoding,
+    symmetric_password: Option<Password>,
+}
+
+#[cfg(not(test))]
+fn string_to_key() -> StringToKey {
+    StringToKey::new_argon2(rand::thread_rng(), 3, 4, 16) // 64 MiB with rpgp's recommended parameter choice
+}
+
+#[cfg(test)]
+fn string_to_key() -> StringToKey {
+    StringToKey::new_argon2(rand::thread_rng(), 1, 1, 10) // fast for unit tests
+}
+
+impl EncryptContentPGP {
+    fn encrypt_bytes(
+        &self,
+        input_stream: &mut dyn InputStream,
+        output_stream: &mut dyn OutputStream,
+        pub_key: Option<&SignedPublicKey>,
+        file_name: String,
+    ) -> Result<(), MinifiError> {
+        if pub_key.is_none() && self.symmetric_password.is_none() {
+            return Err(MinifiError::custom(
+                "No password or public key to encrypt with",
+            ));
+        }
+
+        let mut builder = MessageBuilder::from_reader(file_name, input_stream).seipd_v1(
+            rand::thread_rng(),
+            pgp::crypto::sym::SymmetricKeyAlgorithm::AES256,
+        );
+
+        if let Some(pub_key) = pub_key {
+            match select_encryption_target(pub_key)? {
+                EncryptionTarget::Primary(primary_key) => builder
+                    .encrypt_to_key(rand::thread_rng(), primary_key)
+                    .map_err(MinifiError::other)?,
+                EncryptionTarget::Subkey(subkey) => builder
+                    .encrypt_to_key(rand::thread_rng(), subkey)
+                    .map_err(MinifiError::other)?,
+            };
+        }
+
+        if let Some(password) = &self.symmetric_password {
+            builder
+                .encrypt_with_password(string_to_key(), password)
+                .map_err(MinifiError::other)?;
+        }
+
+        match self.file_encoding {
+            FileEncoding::Ascii => builder
+                .to_armored_writer(rand::thread_rng(), ArmorOptions::default(), output_stream)
+                .map_err(MinifiError::other),
+            FileEncoding::Binary => builder
+                .to_writer(rand::thread_rng(), output_stream)
+                .map_err(MinifiError::other),
+        }
+    }
+}
+
+impl Schedule for EncryptContentPGP {
+    fn schedule<P: GetProperty + GetControllerService, L: Logger>(
+        context: &P,
+        _logger: &L,
+    ) -> Result<Self, MinifiError>
+    where
+        Self: Sized,
+    {
+        let file_encoding = context.get_property(&FILE_ENCODING)?;
+        let symmetric_password = context.get_property(&SYMMETRIC_PASSPHRASE)?;
+
+        let public_key_service = context.get_controller_service(&PUBLIC_KEY_SERVICE)?;
+
+        // Given API support we should check if PUBLIC_KEY_SEARCH is set (without EL)
+        if symmetric_password.is_none() && public_key_service.is_none() {
+            return Err(MinifiError::custom(
+                "Either a password or Public Key Service with Public Key Search should be configured to encrypt files",
+            ));
+        }
+        Ok(EncryptContentPGP {
+            file_encoding,
+            symmetric_password,
+        })
+    }
+}
+
+impl EncryptContentPGP {
+    fn get_public_key<Ctx: GetProperty + GetControllerService>(
+        context: &Ctx,
+    ) -> Result<Option<&SignedPublicKey>, MinifiError> {
+        if let (Some(pub_key_search), Some(public_key_service)) = (
+            context.get_property(&PUBLIC_KEY_SEARCH)?,
+            context.get_controller_service(&PUBLIC_KEY_SERVICE)?,
+        ) {
+            Ok(Some(public_key_service.get(&pub_key_search)?))
+        } else {
+            Ok(None)
+        }
+    }
+}
+
+impl FlowFileStreamTransform for EncryptContentPGP {
+    fn transform<
+        Ctx: GetProperty + GetControllerService + GetAttribute + GetId,
+        LoggerImpl: Logger,
+    >(
+        &self,
+        context: &Ctx,
+        input_stream: &mut dyn InputStream,
+        output_stream: &mut dyn OutputStream,
+        _logger: &LoggerImpl,
+    ) -> Result<TransformStreamResult, ProcessError> {
+        let file_name = match context.get_attribute("filename")? {
+            Some(file_name) => file_name,
+            None => context.get_id()?,
+        };
+        let public_key = Self::get_public_key(context).route_err_to_failure()?;
+
+        self.encrypt_bytes(input_stream, output_stream, public_key, file_name)
+            .route_err_to_failure()?;
+
+        Ok(TransformStreamResult::new(&SUCCESS)
+            .with_attribute(FILE_ENCODING_ATTR.name, self.file_encoding.into_str()))
+    }
+}
+
+mod proc_def {
+    use super::*;
+    use crate::controller_services::public_key_service::PGPPublicKeyService;
+    use crate::utils;
+    use minifi_native::{
+        OutputAttribute, ProcessorDefinition, ProcessorInputRequirement, Property,
+        PropertyDefinition, Relationship, property_definitions,
+    };
+
+    pub(crate) const FILE_ENCODING: Property<FileEncoding> =
+        Property::new("File Encoding", "File Encoding for encryption")
+            .with_default(FileEncoding::Binary.into_str());
+
+    pub(crate) const SYMMETRIC_PASSPHRASE: Property<Option<utils::Password>> = Property::new(
+        "Passphrase",
+        "Passphrase used for encrypting data with Password-Based Encryption",
+    )
+    .sensitive();
+
+    pub(crate) const PUBLIC_KEY_SEARCH: Property<Option<String>> = Property::new(
+        "Public Key Search",
+        "PGP Public Key Search will be used to match against the User ID or Key ID when formatted as uppercase hexadecimal string of 16 characters",
+    ).supports_expression_language();
+
+    pub(crate) const PUBLIC_KEY_SERVICE: Property<Option<PGPPublicKeyService>> = Property::new(
+        "Public Key Service",
+        "PGP Public Key Service for encrypting data with Public Key Encryption",
+    );
+
+    pub(super) const FILE_ENCODING_ATTR: OutputAttribute = OutputAttribute {
+        name: "pgp.file.encoding",
+        relationships: &["success"],
+        description: "File Encoding",
+    };
+
+    pub(super) const SUCCESS: Relationship = Relationship {
+        name: "success",
+        description: "Encryption Succeeded",
+    };
+
+    pub(super) const FAILURE: Relationship = Relationship {
+        name: "failure",
+        description: "Encryption Failed",
+    };
+
+    impl ProcessorDefinition for EncryptContentPGP {
+        const DESCRIPTION: &'static str = "Encrypt contents using OpenPGP.";
+        const INPUT_REQUIREMENT: ProcessorInputRequirement = ProcessorInputRequirement::Required;
+        const SUPPORTS_DYNAMIC_PROPERTIES: bool = false;
+        const SUPPORTS_DYNAMIC_RELATIONSHIPS: bool = false;
+        const OUTPUT_ATTRIBUTES: &'static [OutputAttribute] = &[FILE_ENCODING_ATTR];
+        const RELATIONSHIPS: &'static [Relationship] = &[SUCCESS, FAILURE];
+
+        const PROPERTIES: &[PropertyDefinition] = property_definitions![
+            FILE_ENCODING,
+            SYMMETRIC_PASSPHRASE,
+            PUBLIC_KEY_SEARCH,
+            PUBLIC_KEY_SERVICE,
+        ];
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::controller_services::public_key_service::PGPPublicKeyService;
+    use crate::test_utils;
+    use minifi_native::{
+        ComponentIdentifier, EnableControllerService, IoState, MockControllerServiceContext,
+        MockLogger, MockProcessContext, test,
+    };
+    #[test]
+    fn test_ids() {
+        assert_eq!(
+            EncryptContentPGP::CLASS_NAME,
+            "minifi_pgp::processors::encrypt_content::EncryptContentPGP"
+        );
+        assert_eq!(EncryptContentPGP::GROUP_NAME, "minifi_pgp");
+        assert_eq!(EncryptContentPGP::VERSION, "1.0.0");
+    }
+
+    #[test]
+    fn cannot_schedule_without_password_or_public_key() {
+        assert!(
+            EncryptContentPGP::schedule(&MockProcessContext::new(), &MockLogger::new()).is_err()
+        );
+    }
+
+    fn assert_content(transform_result: &TransformStreamResult, is_ascii: bool) {
+        assert_eq!(transform_result.target_relationship_name(), SUCCESS.name);
+        assert_eq!(transform_result.write_status(), IoState::Ok);
+        assert_eq!(
+            transform_result.get_attribute("pgp.file.encoding").unwrap(),
+            if is_ascii { "ASCII" } else { "BINARY" }
+        );
+    }
+
+    #[test]
+    fn encrypts_via_passphrase() {
+        let mut context = MockProcessContext::new();
+        context
+            .properties
+            .insert(SYMMETRIC_PASSPHRASE.name(), "password");
+
+        let mut result: Vec<u8> = Vec::new();
+        let mut input_stream = std::io::Cursor::new("foo".as_bytes());
+        let processor =
+            EncryptContentPGP::schedule(&context, &MockLogger::new()).expect("should schedule");
+        let transformed_ff = processor
+            .transform(&context, &mut input_stream, &mut result, &MockLogger::new())
+            .expect("should transform");
+
+        assert!(!result.is_ascii());
+        assert_content(&transformed_ff, false);
+    }
+
+    fn public_key_service() -> PGPPublicKeyService {
+        let mut context = MockControllerServiceContext::new();
+        context.properties.insert(
+            "Keyring File".to_string(),
+            test_utils::get_test_key_path("keyring.asc"),
+        );
+
+        PGPPublicKeyService::enable(&context, &MockLogger::new()).expect("should enable")
+    }
+
+    #[test]
+    fn encrypts_ascii_for_alice() {
+        let mut context = MockProcessContext::new();
+        context.properties.extend([
+            ("Public Key Service", "my_controller_service"),
+            ("Public Key Search", "Alice"),
+            ("File Encoding", "ASCII"),
+        ]);
+
+        context.controller_services.insert(
+            "my_controller_service".to_string(),
+            Box::new(public_key_service()),
+        );
+
+        let mut result: Vec<u8> = Vec::new();
+        let mut input_stream = std::io::Cursor::new("foo".as_bytes());
+        let processor =
+            EncryptContentPGP::schedule(&context, &MockLogger::new()).expect("should schedule");
+        let transformed_ff = processor
+            .transform(&context, &mut input_stream, &mut result, &MockLogger::new())
+            .expect("should transform");
+
+        assert!(result.is_ascii());
+        assert_content(&transformed_ff, true);
+    }
+
+    #[test]
+    fn encrypts_binary_for_bob() {
+        let mut context = MockProcessContext::new();
+        context.properties.extend([
+            ("Public Key Service", "my_controller_service"),
+            ("Public Key Search", "Bob"),
+            ("File Encoding", "BINARY"),
+        ]);
+
+        context.controller_services.insert(
+            "my_controller_service".to_string(),
+            Box::new(public_key_service()),
+        );
+
+        let mut result: Vec<u8> = Vec::new();
+        let mut input_stream = std::io::Cursor::new("foo".as_bytes());
+        let processor =
+            EncryptContentPGP::schedule(&context, &MockLogger::new()).expect("should schedule");
+        let transformed_ff = processor
+            .transform(&context, &mut input_stream, &mut result, &MockLogger::new())
+            .expect("should transform");
+
+        assert!(!result.is_ascii());
+        assert_content(&transformed_ff, false);
+    }
+
+    #[test]
+    fn cannot_encrypt_for_carol() {
+        let mut context = MockProcessContext::new();
+        context.properties.extend([
+            ("Public Key Service", "my_controller_service"),
+            ("Public Key Search", "Carol"),
+        ]);
+
+        context.controller_services.insert(
+            "my_controller_service".to_string(),
+            Box::new(public_key_service()),
+        );
+
+        let mut result: Vec<u8> = Vec::new();
+        let mut input_stream = std::io::Cursor::new("foo".as_bytes());
+        let processor =
+            EncryptContentPGP::schedule(&context, &MockLogger::new()).expect("should schedule");
+        let res = processor.transform(&context, &mut input_stream, &mut result, &MockLogger::new());
+
+        test::assert_routed_to(res, &FAILURE);
+    }
+
+    #[test]
+    fn configured_public_key_miss_fails_even_with_password() {
+        let mut context = MockProcessContext::new();
+        context.properties.extend([
+            ("Public Key Service", "my_controller_service"),
+            ("Public Key Search", "Carol"),
+            ("Symmetric Password", "password"),
+        ]);
+
+        context.controller_services.insert(
+            "my_controller_service".to_string(),
+            Box::new(public_key_service()),
+        );
+
+        let mut result: Vec<u8> = Vec::new();
+        let mut input_stream = std::io::Cursor::new("foo".as_bytes());
+        let processor =
+            EncryptContentPGP::schedule(&context, &MockLogger::new()).expect("should schedule");
+        let res = processor.transform(&context, &mut input_stream, &mut result, &MockLogger::new());
+
+        test::assert_routed_to(res, &FAILURE);
+    }
+}
diff --git a/minifi_rust/extensions/minifi_pgp/src/processors/mod.rs b/minifi_rust/extensions/minifi_pgp/src/processors/mod.rs
new file mode 100644
index 000000000..a22c7b1b8
--- /dev/null
+++ b/minifi_rust/extensions/minifi_pgp/src/processors/mod.rs
@@ -0,0 +1,19 @@
+// 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
+//
+//   https://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.
+
+pub(crate) mod decrypt_content;
+pub(crate) mod encrypt_content;
diff --git a/minifi_rust/extensions/minifi_pgp/src/test_utils/mod.rs b/minifi_rust/extensions/minifi_pgp/src/test_utils/mod.rs
new file mode 100644
index 000000000..1ceb14cab
--- /dev/null
+++ b/minifi_rust/extensions/minifi_pgp/src/test_utils/mod.rs
@@ -0,0 +1,32 @@
+// 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
+//
+//   https://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.
+
+use std::path::PathBuf;
+
+pub fn get_test_key_path(filename: &str) -> String {
+    let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
+    path.push("test_keys");
+    path.push(filename);
+    path.display().to_string()
+}
+
+pub fn get_test_message(filename: &str) -> Vec<u8> {
+    let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
+    path.push("test_messages");
+    path.push(filename);
+    std::fs::read(path).expect("test message should be readable")
+}
diff --git a/minifi_rust/extensions/minifi_pgp/src/utils.rs b/minifi_rust/extensions/minifi_pgp/src/utils.rs
new file mode 100644
index 000000000..7528e06b3
--- /dev/null
+++ b/minifi_rust/extensions/minifi_pgp/src/utils.rs
@@ -0,0 +1,80 @@
+// 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
+//
+//   https://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.
+
+use minifi_native::{
+    MinifiError, PropertyConstraints, PropertySchema, PropertyType, StandardPropertyValidator,
+};
+
+pub(crate) struct Password {}
+
+impl PropertySchema for Password {
+    const CONSTRAINT: Option<PropertyConstraints> = Some(PropertyConstraints::Validator(
+        StandardPropertyValidator::NonBlankValidator,
+    ));
+    const IS_REQUIRED: bool = true;
+}
+
+impl PropertyType for Password {
+    type Output = pgp::types::Password;
+
+    fn parse(s: &str) -> Result<Self::Output, MinifiError> {
+        Ok(pgp::types::Password::from(s))
+    }
+}
+
+/// A newline separated list of passwords, each of which is tried in turn when unlocking a key.
+pub(crate) struct Passwords {}
+
+impl PropertySchema for Passwords {
+    const CONSTRAINT: Option<PropertyConstraints> = Some(PropertyConstraints::Validator(
+        StandardPropertyValidator::NonBlankValidator,
+    ));
+    const IS_REQUIRED: bool = true;
+}
+
+impl PropertyType for Passwords {
+    type Output = Vec<pgp::types::Password>;
+
+    fn parse(s: &str) -> Result<Self::Output, MinifiError> {
+        Ok(s.lines()
+            .filter(|line| !line.is_empty())
+            .map(pgp::types::Password::from)
+            .collect())
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn single_password() {
+        assert_eq!(Passwords::parse("hunter2").unwrap().len(), 1);
+    }
+
+    #[test]
+    fn one_password_per_line() {
+        let passwords = Passwords::parse("alice-pw\nbob-pw").unwrap();
+        assert_eq!(passwords.len(), 2);
+    }
+
+    #[test]
+    fn blank_lines_are_dropped() {
+        let passwords = Passwords::parse("alice-pw\n\nbob-pw\n").unwrap();
+        assert_eq!(passwords.len(), 2);
+    }
+}
diff --git a/minifi_rust/extensions/minifi_pgp/test_keys/README.txt b/minifi_rust/extensions/minifi_pgp/test_keys/README.txt
new file mode 100644
index 000000000..86984f128
--- /dev/null
+++ b/minifi_rust/extensions/minifi_pgp/test_keys/README.txt
@@ -0,0 +1,59 @@
+Testing keys v4
+------------------------
+uid           [ultimate] Alice <al...@example.com>
+keyid         BCCE3FDFBA019D7E
+passphrase    whiterabbit
+RSA, the primary key carries the encrypt capability itself, no subkeys
+
+uid           [ultimate] Bob Personal <bo...@home.io>
+uid           [ultimate] Bob Primary <bo...@work.com>
+keyid         A06749BA4F34B0E5
+no passphrase
+
+uid           [ultimate] Dave <da...@example.com>
+keyid         297B6A88887FB64F
+passphrase    gardenparty
+Ed25519 sign-only primary key plus a Cv25519 encryption subkey, which is the
+layout `gpg --gen-key` produces by default. Encrypting to this key only works
+if the encryption subkey is selected rather than the primary key.
+  gpg --quick-generate-key 'Dave <da...@example.com>' ed25519 sign never
+  gpg --quick-add-key <fingerprint> cv25519 encr never
+
+uid           [ultimate] Erin <er...@example.com>
+keyid         8C96441440A22F74
+no passphrase
+Ed25519 sign-only primary key with no subkeys at all, so it cannot be used for
+encryption. Public key only.
+  gpg --quick-generate-key 'Erin <er...@example.com>' ed25519 sign never
+
+uid           [ultimate] Bob Personal <bo...@home.io.attacker.test>
+keyid         756FC2EBECB8747C
+no passphrase
+A look-alike of Bob whose User ID has Bob's own address as a prefix, so that a
+"bob@home.io" search matches both keys. Only used to build
+ambiguous_keyring.gpg. Public key only.
+
+Keyrings
+------------------------
+keyring.{asc,gpg}               public:  Alice + Bob
+secret_keyring.{asc,gpg}        secret:  Alice + Bob
+ambiguous_keyring.gpg           public:  Alice + Bob + the look-alike Bob, so
+                                that a "bob@home.io" User ID search is
+                                ambiguous
+mixed_secret_keyring.gpg        secret:  Alice + Dave, i.e. two keys whose
+                                passphrases differ, which requires more than
+                                one Key Password to unlock both
+
+The binary keyrings are concatenations of the individual dearmored keys:
+  gpg --dearmor < keyring.asc > keyring.bin
+  gpg --dearmor < spoofed_bob.asc > spoofed.bin
+  cat keyring.bin spoofed.bin > ambiguous_keyring.gpg
+
+Passphrase protection
+------------------------
+Every passphrase-protected secret key here is deliberately protected with the
+lowest S2K iteration count gpg will emit (65536, its floor). Unlocking a key
+runs the full S2K hash chain, and gpg's own calibrated default of 58720256 means
+56 MiB of SHA-1 per protected packet. Dave has two protected packets and each
+candidate passphrase is tried against each, which made the decrypt_content tests
+take seconds apiece. Keep the low count when regenerating.
diff --git a/minifi_rust/extensions/minifi_pgp/test_keys/alice.asc b/minifi_rust/extensions/minifi_pgp/test_keys/alice.asc
new file mode 100644
index 000000000..ebe21a684
--- /dev/null
+++ b/minifi_rust/extensions/minifi_pgp/test_keys/alice.asc
@@ -0,0 +1,18 @@
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+mQENBGmJ4mMBCADEYKe0I+Mr/IPKlYUVEgb0yC+iRRBfkhPfMQjbjfM3sD2jyf0g
+k983pVWF5I4Tl3AFW+D2wAJCayFnIYQWaMnK133EdxZE61ewhvS9g3Zk2Q1JzO6F
+B/w4hfCSlPkK82j3Xrd1U0ZJ+YP60DucYlanFPu8+Iw7reusOT9JOJWazzCo4XQX
+ImXomT+KVFsN9UDaKv0mSoH5VgG6thQb6veajcYhrN7rU7isTA2hICxo1nLlnCpy
+7LbAcN9sP1r5Ukpw0WsXKItol0f29p72biFrLbL7670lRxl/CuHTbHYpbCo7Cdng
+R1ieQ3yEqX4qqrKbNocpylCrSSuc78F7pxYNABEBAAG0GUFsaWNlIDxhbGljZUBl
+eGFtcGxlLmNvbT6JAVIEEwEIADwWIQSZnIioWmY7HCCheVW8zj/fugGdfgUCaYni
+YwIbLwULCQgHAgMiAgEGFQoJCAsCBBYCAwECHgcCF4AACgkQvM4/37oBnX7RUQf7
+BZV8ZPlZxbQaZ8kMabC9meNVaYTOWIjJpDqJ7XqG6mMwvzeA3hvtgQOEEQTYbUcw
++ItlN9XQVrlNAyJZVGJQ+2UXp4XRmdogyShE/zFv/NkxvccfzC/BD5jMy88BS8lu
+Gv8bANioE8bF/IwC538A5/6oPN1weQ4OpjcBfeYoyDD32sMc2P5uAWe9pLTAR+vz
+F5mWG4K6TWrfH8/YuB6CCQSCr17N9ZPP2rpcPa2miyzQGJy5wdMOiahBFfdvHACO
+bJXeKYcxZdLs7ImMw78IbbVkzmEKzCStLTqntL8Bsh+eu4nvescoYkMPUzW1E3P3
+BavsyDPmu/yP03Dpg0TubA==
+=+8XC
+-----END PGP PUBLIC KEY BLOCK-----
diff --git a/minifi_rust/extensions/minifi_pgp/test_keys/alice.gpg b/minifi_rust/extensions/minifi_pgp/test_keys/alice.gpg
new file mode 100644
index 000000000..8ef375e68
Binary files /dev/null and b/minifi_rust/extensions/minifi_pgp/test_keys/alice.gpg differ
diff --git a/minifi_rust/extensions/minifi_pgp/test_keys/alice_private.asc b/minifi_rust/extensions/minifi_pgp/test_keys/alice_private.asc
new file mode 100644
index 000000000..6b7ca4d44
--- /dev/null
+++ b/minifi_rust/extensions/minifi_pgp/test_keys/alice_private.asc
@@ -0,0 +1,32 @@
+-----BEGIN PGP PRIVATE KEY BLOCK-----
+
+lQPGBGmJ4mMBCADEYKe0I+Mr/IPKlYUVEgb0yC+iRRBfkhPfMQjbjfM3sD2jyf0g
+k983pVWF5I4Tl3AFW+D2wAJCayFnIYQWaMnK133EdxZE61ewhvS9g3Zk2Q1JzO6F
+B/w4hfCSlPkK82j3Xrd1U0ZJ+YP60DucYlanFPu8+Iw7reusOT9JOJWazzCo4XQX
+ImXomT+KVFsN9UDaKv0mSoH5VgG6thQb6veajcYhrN7rU7isTA2hICxo1nLlnCpy
+7LbAcN9sP1r5Ukpw0WsXKItol0f29p72biFrLbL7670lRxl/CuHTbHYpbCo7Cdng
+R1ieQ3yEqX4qqrKbNocpylCrSSuc78F7pxYNABEBAAH+BwMC47UFwy38Rctg1BsL
+M8qlY9DTZ4OSEupNBRmlzoBs8VDNNnMpm97qvxJfG4LVb8/rkqTqsPs4BBBB2rkU
+afdstA9z6ctk4fAbbP4+EO5ZNWx4vX0+LbwyfACVZe+lV0XUC7UddKmG4lNzi+3N
+aOOQSAUsPVYcKBGWh7QvqjQ6FHsLcCTCJVCwJCJ3HfyLx+RVq64k2kakCRpVXpU7
+q2y3VHzICXney7GtlkuGLOZBjxi/82BQCMn4KqH1BjlA/bE6kCnThisV8C02gg75
+7BKKbzSxmoyMDCtLzPoECEoZOihpNtND/px6yh6QOTYIy/bAv8siPTVEe/ht/4Gz
+FEOvYMWkYxaZEiTdaBTXyG5BaIs4ZwOnJAjHQX7De3dbbQGCOIdCvHCJwrtnNMCk
+878k9dFm68a1NGK3gEWY7b66sYOXbZDA+ADv3wH2uhE/vMG8yuTfVA7Vb7saUqbf
+SVAN3kGgd04T1z1bVGRLMG0q6/2Im+pveTTyPBhMW0hAmZMHXioUi49E78FVBYhy
+3fa1NbXIVP2VUXISaPyzU5NLoKJ25PQT0paKB83ICaxWBNC7Htljack30pUgsaht
+aPVITGBefrU9dUX0dn+ZbvlhKivQvDi/pZ6ybpMUZ7nvBpCchMwGfpTo+9E6gfMI
+PDj0WPOw/8wy5GL9zuYCYzVbjCINJRUV2CYDkcIkXiyGAkbdaVyL/e4b1J9Zd32L
++5QL59ZnQZ14NEDTavzTL6QkKG+Pu6Js2zVm23ycCprUC7l7vIYSS4li3hKRMcb7
+izm7rOephgfeCsCJNoGWIBORhSqmBtT3A9Ui6TZcFnyMq2oY6+eW2lppMq8uYH/n
+1+LFc3jIEb4CPMCqwItrGfpAmpfqEdzg4rh6g755KADdMmzygk18CiThb42mVZTj
+sFRsEKnlv/46tBlBbGljZSA8YWxpY2VAZXhhbXBsZS5jb20+iQFSBBMBCAA8FiEE
+mZyIqFpmOxwgoXlVvM4/37oBnX4FAmmJ4mMCGy8FCwkIBwIDIgIBBhUKCQgLAgQW
+AgMBAh4HAheAAAoJELzOP9+6AZ1+0VEH+wWVfGT5WcW0GmfJDGmwvZnjVWmEzliI
+yaQ6ie16hupjML83gN4b7YEDhBEE2G1HMPiLZTfV0Fa5TQMiWVRiUPtlF6eF0Zna
+IMkoRP8xb/zZMb3HH8wvwQ+YzMvPAUvJbhr/GwDYqBPGxfyMAud/AOf+qDzdcHkO
+DqY3AX3mKMgw99rDHNj+bgFnvaS0wEfr8xeZlhuCuk1q3x/P2LgeggkEgq9ezfWT
+z9q6XD2tposs0BicucHTDomoQRX3bxwAjmyV3imHMWXS7OyJjMO/CG21ZM5hCswk
+rS06p7S/AbIfnruJ73rHKGJDD1M1tRNz9wWr7Mgz5rv8j9Nw6YNE7mw=
+=+91n
+-----END PGP PRIVATE KEY BLOCK-----
diff --git a/minifi_rust/extensions/minifi_pgp/test_keys/alice_private.gpg b/minifi_rust/extensions/minifi_pgp/test_keys/alice_private.gpg
new file mode 100644
index 000000000..c0cfc2c52
Binary files /dev/null and b/minifi_rust/extensions/minifi_pgp/test_keys/alice_private.gpg differ
diff --git a/minifi_rust/extensions/minifi_pgp/test_keys/ambiguous_keyring.gpg b/minifi_rust/extensions/minifi_pgp/test_keys/ambiguous_keyring.gpg
new file mode 100644
index 000000000..c0e9da5ac
Binary files /dev/null and b/minifi_rust/extensions/minifi_pgp/test_keys/ambiguous_keyring.gpg differ
diff --git a/minifi_rust/extensions/minifi_pgp/test_keys/bob_private.asc b/minifi_rust/extensions/minifi_pgp/test_keys/bob_private.asc
new file mode 100644
index 000000000..09fc1bc99
--- /dev/null
+++ b/minifi_rust/extensions/minifi_pgp/test_keys/bob_private.asc
@@ -0,0 +1,71 @@
+-----BEGIN PGP PRIVATE KEY BLOCK-----
+
+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
+=ePbg
+-----END PGP PRIVATE KEY BLOCK-----
diff --git a/minifi_rust/extensions/minifi_pgp/test_keys/bob_private.gpg b/minifi_rust/extensions/minifi_pgp/test_keys/bob_private.gpg
new file mode 100644
index 000000000..a39370243
Binary files /dev/null and b/minifi_rust/extensions/minifi_pgp/test_keys/bob_private.gpg differ
diff --git a/minifi_rust/extensions/minifi_pgp/test_keys/dave.asc b/minifi_rust/extensions/minifi_pgp/test_keys/dave.asc
new file mode 100644
index 000000000..e413d1fba
--- /dev/null
+++ b/minifi_rust/extensions/minifi_pgp/test_keys/dave.asc
@@ -0,0 +1,14 @@
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+mDMEarPhDRYJKwYBBAHaRw8BAQdAIutX9mnyeaFBjHNkOMPq9UsWaoZB3An5wfHn
+pns7UJ+0F0RhdmUgPGRhdmVAZXhhbXBsZS5jb20+iK8EExYKAFcWIQRTkbiPC69l
+G2IDzoApe2qIiH+2TwUCarPhDRsUgAAAAAAEAA5tYW51MiwyLjUrMS4xMiwwLDMC
+GwMFCwkIBwICIgIGFQoJCAsCBBYCAwECHgcCF4AACgkQKXtqiIh/tk8qLgEA7oKJ
+95fgapuSqFLqDa2gAemjXHgeox4Jn2LayST3zrYA/ji0PvUzFxuFrgrl5eCGZfmr
+FM2rPzkppJ5PAxCA/xQBuDgEarPhDhIKKwYBBAGXVQEFAQEHQBKa7zXTR3mYQ7et
+L2oIYWHyRm4kbUmW3E4KGHfyOt4YAwEIB4iUBBgWCgA8FiEEU5G4jwuvZRtiA86A
+KXtqiIh/tk8FAmqz4Q4bFIAAAAAABAAObWFudTIsMi41KzEuMTIsMCwzAhsMAAoJ
+ECl7aoiIf7ZPnK0A/jt6JWNcf8f8GEQn57O8okYgmCgWFOBQCQ24JTiYfgCNAP9p
+Mg/o4PePUr9kkvSNEdK0JKqYAohHzsKj0JszrmWhBg==
+=D4Oc
+-----END PGP PUBLIC KEY BLOCK-----
diff --git a/minifi_rust/extensions/minifi_pgp/test_keys/dave_private.asc b/minifi_rust/extensions/minifi_pgp/test_keys/dave_private.asc
new file mode 100644
index 000000000..1b99d2f08
--- /dev/null
+++ b/minifi_rust/extensions/minifi_pgp/test_keys/dave_private.asc
@@ -0,0 +1,18 @@
+-----BEGIN PGP PRIVATE KEY BLOCK-----
+
+lIYEarPhDRYJKwYBBAHaRw8BAQdAIutX9mnyeaFBjHNkOMPq9UsWaoZB3An5wfHn
+pns7UJ/+BwMC+zyDHE4t43JgXPRBVQSAOrak9PzpECo7QXjcGrqRZBbf1ZTN94Cl
+K20K/z2cc9IwBijYGN1/kgCKMVsU2WgRZ9trzOwFJLzS6+uEK2EVCbQXRGF2ZSA8
+ZGF2ZUBleGFtcGxlLmNvbT6IrwQTFgoAVxYhBFORuI8Lr2UbYgPOgCl7aoiIf7ZP
+BQJqs+ENGxSAAAAAAAQADm1hbnUyLDIuNSsxLjEyLDAsMwIbAwULCQgHAgIiAgYV
+CgkICwIEFgIDAQIeBwIXgAAKCRApe2qIiH+2TyouAQDugon3l+Bqm5KoUuoNraAB
+6aNceB6jHgmfYtrJJPfOtgD+OLQ+9TMXG4WuCuXl4IZl+asUzas/OSmknk8DEID/
+FAGciwRqs+EOEgorBgEEAZdVAQUBAQdAEprvNdNHeZhDt60vaghhYfJGbiRtSZbc
+TgoYd/I63hgDAQgH/gcDAggY5bjBZ8PLYLukHnvX1OQ6PoL1Phk7rlN9qjPCEX0c
++8Db+U6nJ9ckhjL/c9MsWb/quQLDN5WEyVQCmCiWJTX2gx+vpBTxXAD7+f6LozKI
+lAQYFgoAPBYhBFORuI8Lr2UbYgPOgCl7aoiIf7ZPBQJqs+EOGxSAAAAAAAQADm1h
+bnUyLDIuNSsxLjEyLDAsMwIbDAAKCRApe2qIiH+2T5ytAP47eiVjXH/H/BhEJ+ez
+vKJGIJgoFhTgUAkNuCU4mH4AjQD/aTIP6OD3j1K/ZJL0jRHStCSqmAKIR87Co9Cb
+M65loQY=
+=EiF6
+-----END PGP PRIVATE KEY BLOCK-----
diff --git a/minifi_rust/extensions/minifi_pgp/test_keys/erin.asc b/minifi_rust/extensions/minifi_pgp/test_keys/erin.asc
new file mode 100644
index 000000000..21efeead3
--- /dev/null
+++ b/minifi_rust/extensions/minifi_pgp/test_keys/erin.asc
@@ -0,0 +1,10 @@
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+mDMEarPhDxYJKwYBBAHaRw8BAQdA4/aTB+10d9/3Ey4WHi5iXSlP9i9GWqE9xY8k
+c/zzqva0F0VyaW4gPGVyaW5AZXhhbXBsZS5jb20+iK8EExYKAFcWIQTRATOyPqzK
+Yn5F1i6MlkQUQKIvdAUCarPhDxsUgAAAAAAEAA5tYW51MiwyLjUrMS4xMiwwLDMC
+GwMFCwkIBwICIgIGFQoJCAsCBBYCAwECHgcCF4AACgkQjJZEFECiL3SlhAD/cCjS
+ArPCcbLS8v5YUAJvw2M2/9fKSUsUA8zX/by17PUA/R50qJzXFWEbcG73cPda+PdZ
+qiCywhY+HrzMJJ5DE7wC
+=xbRI
+-----END PGP PUBLIC KEY BLOCK-----
diff --git a/minifi_rust/extensions/minifi_pgp/test_keys/garbage.gpg b/minifi_rust/extensions/minifi_pgp/test_keys/garbage.gpg
new file mode 100644
index 000000000..9e603b5c3
Binary files /dev/null and b/minifi_rust/extensions/minifi_pgp/test_keys/garbage.gpg differ
diff --git a/minifi_rust/extensions/minifi_pgp/test_keys/keyring.asc b/minifi_rust/extensions/minifi_pgp/test_keys/keyring.asc
new file mode 100644
index 000000000..fb5c7cc62
--- /dev/null
+++ b/minifi_rust/extensions/minifi_pgp/test_keys/keyring.asc
@@ -0,0 +1,58 @@
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+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
+wA==
+=/AVq
+-----END PGP PUBLIC KEY BLOCK-----
diff --git a/minifi_rust/extensions/minifi_pgp/test_keys/keyring.gpg b/minifi_rust/extensions/minifi_pgp/test_keys/keyring.gpg
new file mode 100644
index 000000000..9bb901d97
Binary files /dev/null and b/minifi_rust/extensions/minifi_pgp/test_keys/keyring.gpg differ
diff --git a/minifi_rust/extensions/minifi_pgp/test_keys/mixed_secret_keyring.gpg b/minifi_rust/extensions/minifi_pgp/test_keys/mixed_secret_keyring.gpg
new file mode 100644
index 000000000..e7183c261
Binary files /dev/null and b/minifi_rust/extensions/minifi_pgp/test_keys/mixed_secret_keyring.gpg differ
diff --git a/minifi_rust/extensions/minifi_pgp/test_keys/secret_keyring.asc b/minifi_rust/extensions/minifi_pgp/test_keys/secret_keyring.asc
new file mode 100644
index 000000000..12a5227a0
--- /dev/null
+++ b/minifi_rust/extensions/minifi_pgp/test_keys/secret_keyring.asc
@@ -0,0 +1,99 @@
+-----BEGIN PGP PRIVATE KEY BLOCK-----
+
+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
+=RDvt
+-----END PGP PRIVATE KEY BLOCK-----
diff --git a/minifi_rust/extensions/minifi_pgp/test_keys/secret_keyring.gpg b/minifi_rust/extensions/minifi_pgp/test_keys/secret_keyring.gpg
new file mode 100644
index 000000000..6a5c0f02a
Binary files /dev/null and b/minifi_rust/extensions/minifi_pgp/test_keys/secret_keyring.gpg differ
diff --git a/minifi_rust/extensions/minifi_pgp/test_keys/spoofed_bob.asc b/minifi_rust/extensions/minifi_pgp/test_keys/spoofed_bob.asc
new file mode 100644
index 000000000..ea03803a6
--- /dev/null
+++ b/minifi_rust/extensions/minifi_pgp/test_keys/spoofed_bob.asc
@@ -0,0 +1,19 @@
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+mQENBGqz4vkBCAC79KLH64ayyRJmSATxXWiaFBhYZIAcYz/AC4159XMMfbAhtaRr
+UvCiLjmlPDuA1QZtpR139v8UyTxmlkp5xul+Vgjd7dKc1FRFSdJKZVni2RxnC0Cr
+mD+zIzW2qS8kV4tcuxqE2mGVHlpUkivO41HNYnt2Ks5TNcochQMe/3gi+aHko6Pn
+8VlInu4aQx7wnDJBaKNNMajpGJgL/zCgw0TgG0v/m4LIgHFE9RE+bXT2YUpMIYVh
+o6vG82C0xy8uxCMcyHgbic57JsTgRF0TJlJiLn3eEjenjsjARqFEaniIAPvJ99JR
+wVB1v0es8dHfBx00+4XPU67d71l2VoLFHJXHABEBAAG0KEJvYiBQZXJzb25hbCA8
+Ym9iQGhvbWUuaW8uYXR0YWNrZXIudGVzdD6JAW0EEwEIAFcWIQR41anBL61W0dnR
+hWt1b8Lr7Lh0fAUCarPi+RsUgAAAAAAEAA5tYW51MiwyLjUrMS4xMiwwLDMCGw0F
+CwkIBwICIgIGFQoJCAsCBBYCAwECHgcCF4AACgkQdW/C6+y4dHzgPAf/c9R0Oo7t
+pVmfcE1buMa5jopGAugLYsGI4ChhjYILihPw3aCRMW+05wlVYJREAgXQmaigd99a
+Sdt0bqLTIYcY7xKlQuYRc0lJilo/xmak2rm2EJzVM13zGH7GK0pEXr9Mt9vLjdpe
+j0rbLIup9pZYYPtpYk0kz81T2GLqeSYnR7Wdm99Phdagrn3/RY8tzYt/+F6z8JTd
+yF+E955OsLE20xY4dS5j5GShf4xvjNCWhcLfst8Q2ZAyPIGcbpBGP4ww43jFIvjO
+q3wSh+VK3eSnB2gfy+sbwO24zL79udsPvT8a1NGUVjZidseYBvqAzs4m8e724l5F
+EgkpqCOdeyN8hg==
+=nqES
+-----END PGP PUBLIC KEY BLOCK-----
diff --git a/minifi_rust/extensions/minifi_pgp/test_keys/truncated.asc b/minifi_rust/extensions/minifi_pgp/test_keys/truncated.asc
new file mode 100644
index 000000000..d25ddfb62
--- /dev/null
+++ b/minifi_rust/extensions/minifi_pgp/test_keys/truncated.asc
@@ -0,0 +1,10 @@
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+mQENBGmJ4OIBCACz9RXNN6lFaUi0b4V6PTyjc27g9G0OCBMy6H/lcjROMGupqPm1
+9QzEzTIrxkc1LlPx31qzb6SwzQWkKiDnmObcZzG43Yiz1aD0YOqJsHBb9klrdWFx
+VbGTtaDmZg/xAS+VseYTijiucydURPzIKDb25vWl7r+iAdhZY3eo8Zif7g7LDpU6
+hsqAQOVgIGCokbbS4GFTeOIl6uwS1Gchq40vY5AM7o4/AObANNstyROgQrQqq19Y
+QEjnLT6GsxF6jpbrcb+8No6JWJSaqhDjIVug+psaeuqruQkN6o3B85izGk0fu4QD
+kSYGW3/A9ArGrLhGMtFnTyo/fg9sEGqWxLoJABEBAAG0GUFsaWNlIDxhbGljZUBl
+eGFtcGxlLmNvbT6JAVIEEwEIADwWIQQR1fT4Ba73eK2U4NIbsOxL81Ml9gUCaYng
+4gIbLwU
\ No newline at end of file
diff --git a/minifi_rust/extensions/minifi_pgp/test_keys/truncated_private.asc b/minifi_rust/extensions/minifi_pgp/test_keys/truncated_private.asc
new file mode 100644
index 000000000..9cc58539a
--- /dev/null
+++ b/minifi_rust/extensions/minifi_pgp/test_keys/truncated_private.asc
@@ -0,0 +1,17 @@
+-----BEGIN PGP PRIVATE KEY BLOCK-----
+
+lQPGBGmJ4OIBCACz9RXNN6lFaUi0b4V6PTyjc27g9G0OCBMy6H/lcjROMGupqPm1
+9QzEzTIrxkc1LlPx31qzb6SwzQWkKiDnmObcZzG43Yiz1aD0YOqJsHBb9klrdWFx
+VbGTtaDmZg/xAS+VseYTijiucydURPzIKDb25vWl7r+iAdhZY3eo8Zif7g7LDpU6
+hsqAQOVgIGCokbbS4GFTeOIl6uwS1Gchq40vY5AM7o4/AObANNstyROgQrQqq19Y
+QEjnLT6GsxF6jpbrcb+8No6JWJSaqhDjIVug+psaeuqruQkN6o3B85izGk0fu4QD
+kSYGW3/A9ArGrLhGMtFnTyo/fg9sEGqWxLoJABEBAAH+BwMCYkH6w6nO395gtdQ6
+zQJvZ1itO9NCbRtI20iVkqWmwtr2FwgN8AZ9sGZss6zdpfxh85Ef1kHvP1nkbedk
+/8OmBljPooqKB7MTwCCxOC53Mf6wNMijlBYsY8YUyi4dwaHoxnDFnaCeITSHHehY
+07ifnInvrTkbJ41JzfP124xQ804voehm7merA91Vtpvg/hoYqJ/Sxo22UpTwvuw/
+aKqoetlJWqRk8VmBpcuuVFYcF9jaOPB51WG8fRDj66eINg2zXL49WRvwlUtbAHvS
+cbglkBzMFHqljx0KJWX/QMO64X894eFafVFvSiYf+fn80wv9h7IKjj413itlbF0r
+X+DckGQ9b50XAD3kZgDOMr5dKTGQ9ytChl7hpy38ucQqJM0qRlTSG2mp05qBO+XZ
+CNNE7qvVNJNP5nD/3xkWD8oi+nhmqqg4bZ/QDUcoTVrWW7L1er2fgREJg7xL7xuD
+QYu/T0A6N9PxWefYdEN/jjcLqks/Pjdy3DfGtlDysj88GpLh3diNgQ2EnNgQ32pq
+JmxwEWIQ4VV30Ms1D0Uh7g4Ksq0lq1/LjOll3FSyjr3Ihpesopj
\ No newline at end of file
diff --git a/minifi_rust/extensions/minifi_pgp/test_messages/foo_for_alice.asc b/minifi_rust/extensions/minifi_pgp/test_messages/foo_for_alice.asc
new file mode 100644
index 000000000..94b8561c9
--- /dev/null
+++ b/minifi_rust/extensions/minifi_pgp/test_messages/foo_for_alice.asc
@@ -0,0 +1,12 @@
+-----BEGIN PGP MESSAGE-----
+
+hQEMA7zOP9+6AZ1+AQf/WlAEDriFTKHJfn5KXAi123WGeDJBoRi/etl7GJ8MO5+8
+crdou58wMcqRJ8u3wNgKWDYm+QknLhQK5+3dJajwQeKH18uruTkEmFQB/wArHsOX
+62UhFf2qbAzvUuTH5kPyt1d/Wt51T9+K/xlEPJr+DiK0uHlXZPu7rEnqk9pcKikC
+/dYAuljnkNigDoykHwEBRcBfQu5t/hIe/Bii3wTZPm2w0YneyjOtd7Yq3mDlfmDW
+cy3bdDjuwP4npCxcnHi7WkbElTyCJMybKVwwLjugihGI+4r8itO2wknAT5GDGMQ8
+u2FfOfGnIYTK2mBAQgyM7gtBX2qS28uWYlj5gehngdRJAQkCEFMqwWDAaUXAi6QU
+xhn/O+wEEpUVYnGuEpIqG9KTW3qYl+vTxLkeNzg2NL255QP9gLbdlKFJYkC60c4I
+JJXhkwkPTSC+Xw==
+=UAju
+-----END PGP MESSAGE-----
diff --git a/minifi_rust/extensions/minifi_pgp/test_messages/foo_for_alice.gpg b/minifi_rust/extensions/minifi_pgp/test_messages/foo_for_alice.gpg
new file mode 100644
index 000000000..bd3d9b476
Binary files /dev/null and b/minifi_rust/extensions/minifi_pgp/test_messages/foo_for_alice.gpg differ
diff --git a/minifi_rust/extensions/minifi_pgp/test_messages/password_encrypted_foo.asc b/minifi_rust/extensions/minifi_pgp/test_messages/password_encrypted_foo.asc
new file mode 100644
index 000000000..22d639e71
--- /dev/null
+++ b/minifi_rust/extensions/minifi_pgp/test_messages/password_encrypted_foo.asc
@@ -0,0 +1,6 @@
+-----BEGIN PGP MESSAGE-----
+
+jA0ECQMCU2B2LnRTkyNg0jkBhgVPotvo6S9iLOTWhzglgsjR/6QB2v7vUNImzkh7
+fjhd17fG5tjB1RPRgW3bR12BidV6TQKuwLs=
+=AXoJ
+-----END PGP MESSAGE-----
diff --git a/minifi_rust/extensions/minifi_pgp/test_messages/password_encrypted_foo.gpg b/minifi_rust/extensions/minifi_pgp/test_messages/password_encrypted_foo.gpg
new file mode 100644
index 000000000..40ab6a354
--- /dev/null
+++ b/minifi_rust/extensions/minifi_pgp/test_messages/password_encrypted_foo.gpg
@@ -0,0 +1 @@
+� 	��t�?`�9���B�2;��Œ����g]N)�!r2�!F���nl./��a�9._�}_2�,�ɴ��
\ No newline at end of file
diff --git a/minifi_rust/extensions/minifi_rs_playground/minifi_rs_playground.md b/minifi_rust/extensions/minifi_rs_playground/minifi_rs_playground.md
index e05adf0e0..74ecd008f 100644
--- a/minifi_rust/extensions/minifi_rs_playground/minifi_rs_playground.md
+++ b/minifi_rust/extensions/minifi_rs_playground/minifi_rs_playground.md
@@ -218,6 +218,7 @@ In the list below, the names of required properties appear in bold. Any other pr
 
 | Name                               | Default Value | Allowable Values  | Description                                |
 |------------------------------------|---------------|-------------------|--------------------------------------------|
+| Dummy Controller Service           |               |                   | Optional dummy controller service          |
 | **Lorem Ipsum Controller Service** |               |                   | Name of the lorem ipsum controller service |
 | **Write Method**                   | Buffer        | Buffer<br/>Stream | Which API to test                          |
 
diff --git a/minifi_rust/extensions/minifi_rs_playground/src/processors/asciify_german/tests.rs b/minifi_rust/extensions/minifi_rs_playground/src/processors/asciify_german/tests.rs
index 52863ac6c..619d40d3d 100644
--- a/minifi_rust/extensions/minifi_rs_playground/src/processors/asciify_german/tests.rs
+++ b/minifi_rust/extensions/minifi_rs_playground/src/processors/asciify_german/tests.rs
@@ -17,7 +17,7 @@
 
 use super::*;
 use crate::processors::asciify_german::relationships::SUCCESS;
-use minifi_native::{IoState, MockLogger, MockProcessContext};
+use minifi_native::{IoState, MockLogger, MockProcessContext, test};
 use std::io::BufReader;
 
 #[test]
@@ -84,8 +84,5 @@ fn truncated_umlaut_at_eof_routes_to_failure() {
     let mut output_vec: Vec<u8> = Vec::new();
 
     let result = asciify_german.transform(&context, &mut input_stream, &mut output_vec, &logger);
-    match result {
-        Err(ProcessError::Route(route)) => assert_eq!(route.relationship, FAILURE.name),
-        other => panic!("expected a route error to failure, got {other:?}"),
-    }
+    test::assert_routed_to(result, &FAILURE);
 }
diff --git a/minifi_rust/extensions/minifi_rs_playground/src/processors/put_file/unix_permissions.rs b/minifi_rust/extensions/minifi_rs_playground/src/processors/put_file/unix_permissions.rs
index 8ded233d4..68a54a7e2 100644
--- a/minifi_rust/extensions/minifi_rs_playground/src/processors/put_file/unix_permissions.rs
+++ b/minifi_rust/extensions/minifi_rs_playground/src/processors/put_file/unix_permissions.rs
@@ -15,9 +15,8 @@
 // specific language governing permissions and limitations
 // under the License.
 
+#[cfg(unix)]
 use minifi_native::{MinifiError, PropertyConstraints, PropertySchema, PropertyType};
-use std::path::Path;
-
 #[cfg(unix)]
 use std::os::unix::fs::PermissionsExt;
 
@@ -48,14 +47,14 @@ pub(super) struct PutFileUnixPermissions {
 
 #[cfg(unix)]
 impl PutFileUnixPermissions {
-    pub(crate) fn set_directory_permissions(&self, path: &Path) -> std::io::Result<()> {
+    pub(crate) fn set_directory_permissions(&self, path: &std::path::Path) -> std::io::Result<()> {
         if let Some(permissions) = self.directory_permissions.clone() {
             return std::fs::set_permissions(path, permissions);
         }
         Ok(())
     }
 
-    pub(crate) fn set_file_permissions(&self, file: &Path) -> std::io::Result<()> {
+    pub(crate) fn set_file_permissions(&self, file: &std::path::Path) -> std::io::Result<()> {
         if let Some(permissions) = self.file_permissions.clone() {
             return std::fs::set_permissions(file, permissions);
         }
@@ -69,11 +68,11 @@ pub(crate) struct PutFileUnixPermissions {}
 
 #[cfg(windows)]
 impl PutFileUnixPermissions {
-    pub(crate) fn set_directory_permissions(&self, _path: &Path) -> std::io::Result<()> {
+    pub(crate) fn set_directory_permissions(&self, _path: &std::path::Path) -> std::io::Result<()> {
         Ok(())
     }
 
-    pub(crate) fn set_file_permissions(&self, _file: &Path) -> std::io::Result<()> {
+    pub(crate) fn set_file_permissions(&self, _file: &std::path::Path) -> std::io::Result<()> {
         Ok(())
     }
 }
diff --git a/minifi_rust/minifi_native/src/api/processor.rs b/minifi_rust/minifi_native/src/api/processor.rs
index 8ac4bfb12..413524e29 100644
--- a/minifi_rust/minifi_native/src/api/processor.rs
+++ b/minifi_rust/minifi_native/src/api/processor.rs
@@ -15,12 +15,13 @@
 // specific language governing permissions and limitations
 // under the License.
 
+use crate::GetControllerService;
 use crate::api::{RawProcessor, ThreadingModel};
 use crate::{GetProperty, LogLevel, Logger, MinifiError, ProcessContext};
 use std::marker::PhantomData;
 
 pub trait Schedule {
-    fn schedule<Ctx: GetProperty, L: Logger>(
+    fn schedule<Ctx: GetProperty + GetControllerService, L: Logger>(
         context: &Ctx,
         logger: &L,
     ) -> Result<Self, MinifiError>
diff --git a/minifi_rust/minifi_native/src/api/processor_wrappers/flow_file_stream_transform.rs b/minifi_rust/minifi_native/src/api/processor_wrappers/flow_file_stream_transform.rs
index f300f99b8..1d1bb6765 100644
--- a/minifi_rust/minifi_native/src/api/processor_wrappers/flow_file_stream_transform.rs
+++ b/minifi_rust/minifi_native/src/api/processor_wrappers/flow_file_stream_transform.rs
@@ -24,6 +24,7 @@ use crate::{
     MultiThreaded, OnTriggerResult, OutputStream, ProcessContext, ProcessError, ProcessSession,
     Processor, Relationship, Schedule, SingleThreaded,
 };
+use minifi_native::GetId;
 
 #[derive(Debug)]
 pub struct TransformStreamResult {
@@ -72,7 +73,10 @@ impl TransformStreamResult {
 impl_with_attributes!(TransformStreamResult);
 
 pub trait FlowFileStreamTransform {
-    fn transform<Ctx: GetProperty + GetControllerService + GetAttribute, LoggerImpl: Logger>(
+    fn transform<
+        Ctx: GetProperty + GetControllerService + GetAttribute + GetId,
+        LoggerImpl: Logger,
+    >(
         &self,
         context: &Ctx,
         input_stream: &mut dyn InputStream,
diff --git a/minifi_rust/minifi_native/src/lib.rs b/minifi_rust/minifi_native/src/lib.rs
index b855291a2..4ae26f9c1 100644
--- a/minifi_rust/minifi_native/src/lib.rs
+++ b/minifi_rust/minifi_native/src/lib.rs
@@ -19,6 +19,7 @@ extern crate self as minifi_native;
 mod api;
 pub mod c_ffi;
 pub mod mock;
+pub mod test_utils;
 
 pub use api::errors::{MinifiError, ProcessError, RouteError, RouteErrorExt};
 
@@ -69,6 +70,7 @@ pub use mock::{
     MockControllerServiceContext, MockFlowFile, MockLogger, MockProcessContext, MockProcessSession,
     StdLogger,
 };
+pub use test_utils as test;
 
 #[unsafe(no_mangle)]
 #[allow(non_upper_case_globals)]
diff --git a/minifi_rust/minifi_native/src/test_utils.rs b/minifi_rust/minifi_native/src/test_utils.rs
new file mode 100644
index 000000000..089860498
--- /dev/null
+++ b/minifi_rust/minifi_native/src/test_utils.rs
@@ -0,0 +1,16 @@
+use crate::{ProcessError, Relationship, TransformStreamResult};
+
+pub fn assert_routed_to(
+    res: Result<TransformStreamResult, ProcessError>,
+    expected_relationship: &Relationship,
+) {
+    match res {
+        Err(ProcessError::Route(route)) => {
+            assert_eq!(route.relationship, expected_relationship.name)
+        }
+        Err(other) => {
+            panic!("expected route to '{expected_relationship}', got fatal error: {other:?}")
+        }
+        Ok(_) => panic!("expected route to '{expected_relationship}', got Ok"),
+    }
+}
diff --git a/minifi_rust/minifi_rs_behave/Dockerfile.alpine b/minifi_rust/minifi_rs_behave/Dockerfile.alpine
index 89f2c0198..220c8af4e 100644
--- a/minifi_rust/minifi_rs_behave/Dockerfile.alpine
+++ b/minifi_rust/minifi_rs_behave/Dockerfile.alpine
@@ -1,4 +1,4 @@
-FROM rust:alpine3.22 AS chef
+FROM rust:alpine3.24 AS chef
 RUN apk add --no-cache musl-dev gcc g++ clang-dev lld pkgconfig curl tar && cargo install cargo-chef
 WORKDIR /app