diff --git a/.github/references/ubuntu_22_04_clang_arm_manifest.json b/.github/references/ubuntu_22_04_clang_arm_manifest.json
index 139b3854d7..efc67a971b 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 28fce43e1e..a888c22a5a 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 **Sensitive Property: true** |
+| Keyring | | | Secret Key encoded in ASCII Armor **Sensitive Property: true** |
+| Keyring File | | | File path to PGP Secret Key encoded in binary or ASCII Armor **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 **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 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 4c7705b2a6..a2753b42fe 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 **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 BINARY | File Encoding for encryption |
+| Passphrase | | | Passphrase used for encrypting data with Password-Based Encryption **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 **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 a26a995c5e..b7a1b959ed 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 | CMAKE Flag |
-|----------------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-----------------------------|
-| Archive Extensions | [ApplyTemplate](PROCESSORS.md#applytemplate) [CompressContent](PROCESSORS.md#compresscontent) [ManipulateArchive](PROCESSORS.md#manipulatearchive) [MergeContent](PROCESSORS.md#mergecontent) [FocusArchiveEntry](PROCESSORS.md#focusarchiveentry) [UnfocusArchiveEntry](PROCESSORS.md#unfocusarchiveentry) | -DBUILD_LIBARCHIVE=ON |
-| AWS | [AWSCredentialsService](CONTROLLERS.md#awscredentialsservice) [PutS3Object](PROCESSORS.md#puts3object) [DeleteS3Object](PROCESSORS.md#deletes3object) [FetchS3Object](PROCESSORS.md#fetchs3object) [ListS3](PROCESSORS.md#lists3) [PutKinesisStream](PROCESSORS.md#putkinesisstream) | -DENABLE_AWS=ON |
-| Azure | [AzureStorageCredentialsService](CONTROLLERS.md#azurestoragecredentialsservice) [PutAzureBlobStorage](PROCESSORS.md#putazureblobstorage) [DeleteAzureBlobStorage](PROCESSORS.md#deleteazureblobstorage) [FetchAzureBlobStorage](PROCESSORS.md#fetchazureblobstorage) [ListAzureBlobStorage](PROCESSORS.md#listazureblobstorage) [PutAzureDataLakeStorage](PROCESSORS.md#putazuredatalakestorage) [DeleteAzureDataLakeStorage](PROCESSORS.md#deleteazuredatalakestorage) [FetchAzureDataLakeStorage](PROCESSORS.md#fetchazuredatalakestorage) [ListAzureDataLakeStorage](PROCESSORS.md#listazuredatalakestorage) | -DENABLE_AZURE=ON |
-| CivetWeb | [ListenHTTP](PROCESSORS.md#listenhttp) | -DENABLE_CIVET=ON |
-| Couchbase | [CouchbaseClusterService](CONTROLLERS.md#couchbaseclusterservice) [PutCouchbaseKey](PROCESSORS.md#putcouchbasekey) [GetCouchbaseKey](PROCESSORS.md#getcouchbasekey) | -DENABLE_COUCHBASE=ON |
-| Elasticsearch | [ElasticsearchCredentialsControllerService](CONTROLLERS.md#elasticsearchcredentialscontrollerservice) [PostElasticsearch](PROCESSORS.md#postelasticsearch) | -DENABLE_ELASTICSEARCH=ON |
-| ExecuteProcess (Linux and macOS) | [ExecuteProcess](PROCESSORS.md#executeprocess) | -DENABLE_EXECUTE_PROCESS=ON |
-| Google Cloud Platform | [DeleteGCSObject](PROCESSORS.md#deletegcsobject) [FetchGCSObject](PROCESSORS.md#fetchgcsobject) [GCPCredentialsControllerService](CONTROLLERS.md#gcpcredentialscontrollerservice) [ListGCSBucket](PROCESSORS.md#listgcsbucket) [PutGCSObject](PROCESSORS.md#putgcsobject) | -DENABLE_GCP=ON |
-| Grafana Loki | [PushGrafanaLokiREST](PROCESSORS.md#pushgrafanalokirest) [PushGrafanaLokiGrpc](PROCESSORS.md#pushgrafanalokigrpc) | -DENABLE_GRAFANA_LOKI=ON |
-| Kafka | [PublishKafka](PROCESSORS.md#publishkafka) [ConsumeKafka](PROCESSORS.md#consumekafka) | -DENABLE_KAFKA=ON |
-| Kubernetes (Linux) | [KubernetesControllerService](CONTROLLERS.md#kubernetescontrollerservice) | -DENABLE_KUBERNETES=ON |
-| LlamaCpp | [RunLlamaCppInference](PROCESSORS.md#runllamacppinference) | -DENABLE_LLAMACPP=ON |
-| Lua Scripting | [ExecuteScript](PROCESSORS.md#executescript) | -DENABLE_LUA_SCRIPTING=ON |
-| MQTT | [ConsumeMQTT](PROCESSORS.md#consumemqtt) [PublishMQTT](PROCESSORS.md#publishmqtt) | -DENABLE_MQTT=ON |
-| OPC | [FetchOPCProcessor](PROCESSORS.md#fetchopcprocessor) [PutOPCProcessor](PROCESSORS.md#putopcprocessor) | -DENABLE_OPC=ON |
-| PDH (Windows) | [PerformanceDataMonitor](PROCESSORS.md#performancedatamonitor) | -DENABLE_PDH=ON |
-| ProcFs (Linux) | [ProcFsMonitor](PROCESSORS.md#procfsmonitor) | -DENABLE_PROCFS=ON |
-| Python Scripting | [ExecuteScript](PROCESSORS.md#executescript) [**Custom Python Processors**](extensions/python/PYTHON.md) | -DENABLE_PYTHON_SCRIPTING=ON |
-| SMB (Windows) | [FetchSmb](PROCESSORS.md#fetchsmb) [ListSmb](PROCESSORS.md#listsmb) [PutSmb](PROCESSORS.md#putsmb) | -DENABLE_SMB=ON |
-| SFTP | [FetchSFTP](PROCESSORS.md#fetchsftp) [ListSFTP](PROCESSORS.md#listsftp) [PutSFTP](PROCESSORS.md#putsftp) | -DENABLE_SFTP=ON |
-| SQL | [ExecuteSQL](PROCESSORS.md#executesql) [PutSQL](PROCESSORS.md#putsql) [QueryDatabaseTable](PROCESSORS.md#querydatabasetable) | -DENABLE_SQL=ON |
-| Splunk | [PutSplunkHTTP](PROCESSORS.md#putsplunkhttp) [QuerySplunkIndexingStatus](PROCESSORS.md#querysplunkindexingstatus) | -DENABLE_SPLUNK=ON |
-| Systemd (Linux) | [ConsumeJournald](PROCESSORS.md#consumejournald) | -DENABLE_SYSTEMD=ON |
-| Windows Event Log (Windows) | [ConsumeWindowsEventLog](PROCESSORS.md#consumewindowseventlog) [TailEventLog](PROCESSORS.md#taileventlog) | -DENABLE_WEL=ON |
+| Extension Set | Processors and Controller Services | CMAKE Flag |
+|----------------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-------------------------------------------|
+| Archive Extensions | [ApplyTemplate](PROCESSORS.md#applytemplate) [CompressContent](PROCESSORS.md#compresscontent) [ManipulateArchive](PROCESSORS.md#manipulatearchive) [MergeContent](PROCESSORS.md#mergecontent) [FocusArchiveEntry](PROCESSORS.md#focusarchiveentry) [UnfocusArchiveEntry](PROCESSORS.md#unfocusarchiveentry) | -DBUILD_LIBARCHIVE=ON |
+| AWS | [AWSCredentialsService](CONTROLLERS.md#awscredentialsservice) [PutS3Object](PROCESSORS.md#puts3object) [DeleteS3Object](PROCESSORS.md#deletes3object) [FetchS3Object](PROCESSORS.md#fetchs3object) [ListS3](PROCESSORS.md#lists3) [PutKinesisStream](PROCESSORS.md#putkinesisstream) | -DENABLE_AWS=ON |
+| Azure | [AzureStorageCredentialsService](CONTROLLERS.md#azurestoragecredentialsservice) [PutAzureBlobStorage](PROCESSORS.md#putazureblobstorage) [DeleteAzureBlobStorage](PROCESSORS.md#deleteazureblobstorage) [FetchAzureBlobStorage](PROCESSORS.md#fetchazureblobstorage) [ListAzureBlobStorage](PROCESSORS.md#listazureblobstorage) [PutAzureDataLakeStorage](PROCESSORS.md#putazuredatalakestorage) [DeleteAzureDataLakeStorage](PROCESSORS.md#deleteazuredatalakestorage) [FetchAzureDataLakeStorage](PROCESSORS.md#fetchazuredatalakestorage) [ListAzureDataLakeStorage](PROCESSORS.md#listazuredatalakestorage) | -DENABLE_AZURE=ON |
+| CivetWeb | [ListenHTTP](PROCESSORS.md#listenhttp) | -DENABLE_CIVET=ON |
+| Couchbase | [CouchbaseClusterService](CONTROLLERS.md#couchbaseclusterservice) [PutCouchbaseKey](PROCESSORS.md#putcouchbasekey) [GetCouchbaseKey](PROCESSORS.md#getcouchbasekey) | -DENABLE_COUCHBASE=ON |
+| Elasticsearch | [ElasticsearchCredentialsControllerService](CONTROLLERS.md#elasticsearchcredentialscontrollerservice) [PostElasticsearch](PROCESSORS.md#postelasticsearch) | -DENABLE_ELASTICSEARCH=ON |
+| ExecuteProcess (Linux and macOS) | [ExecuteProcess](PROCESSORS.md#executeprocess) | -DENABLE_EXECUTE_PROCESS=ON |
+| Google Cloud Platform | [DeleteGCSObject](PROCESSORS.md#deletegcsobject) [FetchGCSObject](PROCESSORS.md#fetchgcsobject) [GCPCredentialsControllerService](CONTROLLERS.md#gcpcredentialscontrollerservice) [ListGCSBucket](PROCESSORS.md#listgcsbucket) [PutGCSObject](PROCESSORS.md#putgcsobject) | -DENABLE_GCP=ON |
+| Grafana Loki | [PushGrafanaLokiREST](PROCESSORS.md#pushgrafanalokirest) [PushGrafanaLokiGrpc](PROCESSORS.md#pushgrafanalokigrpc) | -DENABLE_GRAFANA_LOKI=ON |
+| Kafka | [PublishKafka](PROCESSORS.md#publishkafka) [ConsumeKafka](PROCESSORS.md#consumekafka) | -DENABLE_KAFKA=ON |
+| Kubernetes (Linux) | [KubernetesControllerService](CONTROLLERS.md#kubernetescontrollerservice) | -DENABLE_KUBERNETES=ON |
+| LlamaCpp | [RunLlamaCppInference](PROCESSORS.md#runllamacppinference) | -DENABLE_LLAMACPP=ON |
+| Lua Scripting | [ExecuteScript](PROCESSORS.md#executescript) | -DENABLE_LUA_SCRIPTING=ON |
+| MQTT | [ConsumeMQTT](PROCESSORS.md#consumemqtt) [PublishMQTT](PROCESSORS.md#publishmqtt) | -DENABLE_MQTT=ON |
+| OPC | [FetchOPCProcessor](PROCESSORS.md#fetchopcprocessor) [PutOPCProcessor](PROCESSORS.md#putopcprocessor) | -DENABLE_OPC=ON |
+| PDH (Windows) | [PerformanceDataMonitor](PROCESSORS.md#performancedatamonitor) | -DENABLE_PDH=ON |
+| PGP | [EncryptContentPGP](PROCESSORS.md#encryptcontentpgp) [DecryptContentPGP](PROCESSORS.md#decryptcontentpgp) [PGPPublicKeyService](CONTROLLERS.md#pgppublickeyservice) [PGPPrivateKeyService](CONTROLLERS.md#pgpprivatekeyservice) | -DMINIFI_EXTENSION_PGP=ON -DMINIFI_RUST=ON |
+| ProcFs (Linux) | [ProcFsMonitor](PROCESSORS.md#procfsmonitor) | -DENABLE_PROCFS=ON |
+| Python Scripting | [ExecuteScript](PROCESSORS.md#executescript) [**Custom Python Processors**](extensions/python/PYTHON.md) | -DENABLE_PYTHON_SCRIPTING=ON |
+| SMB (Windows) | [FetchSmb](PROCESSORS.md#fetchsmb) [ListSmb](PROCESSORS.md#listsmb) [PutSmb](PROCESSORS.md#putsmb) | -DENABLE_SMB=ON |
+| SFTP | [FetchSFTP](PROCESSORS.md#fetchsftp) [ListSFTP](PROCESSORS.md#listsftp) [PutSFTP](PROCESSORS.md#putsftp) | -DENABLE_SFTP=ON |
+| SQL | [ExecuteSQL](PROCESSORS.md#executesql) [PutSQL](PROCESSORS.md#putsql) [QueryDatabaseTable](PROCESSORS.md#querydatabasetable) | -DENABLE_SQL=ON |
+| Splunk | [PutSplunkHTTP](PROCESSORS.md#putsplunkhttp) [QuerySplunkIndexingStatus](PROCESSORS.md#querysplunkindexingstatus) | -DENABLE_SPLUNK=ON |
+| Systemd (Linux) | [ConsumeJournald](PROCESSORS.md#consumejournald) | -DENABLE_SYSTEMD=ON |
+| Windows Event Log (Windows) | [ConsumeWindowsEventLog](PROCESSORS.md#consumewindowseventlog) [TailEventLog](PROCESSORS.md#taileventlog) | -DENABLE_WEL=ON |
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 7a898a7658..dcd3a2b265 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 63b8a407e1..3166ec5178 100644
--- a/minifi_rust/CMakeLists.txt
+++ b/minifi_rust/CMakeLists.txt
@@ -39,6 +39,10 @@ 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)
add_test(
diff --git a/minifi_rust/Cargo.toml b/minifi_rust/Cargo.toml
index 6dc306f11f..8bde74c44c 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 0000000000..cb8c02ddc4
--- /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 0000000000..f9f6d205fa
--- /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 0000000000..6edd4d3b72
--- /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 0000000000..202a013075
--- /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 0000000000..4652c748a4
--- /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 0000000000..fd56995537
--- /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 0000000000..e72bc0b3f3
--- /dev/null
+++ b/minifi_rust/extensions/minifi_pgp/minifi_pgp.md
@@ -0,0 +1,112 @@
+
+
+## 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 **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 BINARY | File Encoding for encryption |
+| Passphrase | | | Passphrase used for encrypting data with Password-Based Encryption **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 **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 **Sensitive Property: true** |
+| Keyring | | | Secret Key encoded in ASCII Armor **Sensitive Property: true** |
+| Keyring File | | | File path to PGP Secret Key encoded in binary or ASCII Armor **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 **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 0000000000..173f7d4365
--- /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, 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 0000000000..d367d404f2
--- /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 = None;
+ const IS_REQUIRED: bool = true;
+}
+
+impl PropertyType for SecretKeyFile {
+ type Output = Vec;
+
+ fn parse(s: &str) -> Result {
+ parse_key_file(s, KeyKind::Secret)
+ }
+}
+
+pub(crate) struct PublicKeyFile {}
+impl PropertySchema for PublicKeyFile {
+ const CONSTRAINT: Option = None;
+ const IS_REQUIRED: bool = true;
+}
+
+impl PropertyType for PublicKeyFile {
+ type Output = Vec;
+
+ fn parse(s: &str) -> Result {
+ 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 0000000000..26481bac92
--- /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 = 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 0000000000..70acdcb27b
--- /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(
+ input: &str,
+ kind: KeyKind,
+) -> Result, MinifiError> {
+ let mut keys: Vec = 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(
+ path: &str,
+ kind: KeyKind,
+) -> Result, MinifiError> {
+ let mut keys: Vec = 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(
+ context: &Ctx,
+ file_property: &Property