Security: PGP Decryption and Signature Verification¶
craft-easy-file-import supports pre-flight cryptographic operations that run before any parsing: PGP/GPG decryption of encrypted files and detached PGP signature verification. Both are configured per template via CryptoConfig.
How the transform chain works¶
Before a file is parsed, the runner passes it through a TransformChain. The default chain applies transforms in this order, looping until no more apply:
MagicByteDetector— detects MIME type from file magic bytesPgpDecryptor— decrypts if MIME isapplication/pgp-encryptedGzipDecompressorBzip2DecompressorZipExtractorTarExtractor
The loop means nested formats are unwrapped automatically. A file named payments.csv.gz.pgp is processed as: PGP decrypt → gunzip → CSV parse — without any special configuration.
Signature verification (SignatureVerifier) runs separately, before the transform chain, because it must operate on the original encrypted/compressed bytes.
PGP/GPG decryption¶
Installation¶
The decryptor uses pgpy (pure Python). For high-throughput production deployments, pgpy works without system dependencies.
Template configuration¶
from craft_easy_file_import.models import ImportTemplate, CryptoConfig
template = ImportTemplate(
id="bank-payments",
format="csv",
crypto=CryptoConfig(
decryption_credential="payments_pgp_private_key",
),
)
decryption_credential is the name of a key in the credential store whose value is an ASCII-armored PGP private key:
Passphrase-protected keys¶
If the private key is protected by a passphrase, store the passphrase under a credential named {credential_name}.passphrase. For example, if decryption_credential = "payments_pgp_private_key", store the passphrase under payments_pgp_private_key.passphrase.
The decryptor fetches the passphrase credential silently and calls key.unlock(passphrase) before decrypting. If the passphrase credential does not exist, decryption is attempted without a passphrase.
Key management¶
Keys are fetched from the credential store at runtime on every file — no in-process key cache. This means credential rotation takes effect on the next file without restarting the runner.
The credential store is injected at startup:
from craft_easy_file_import.transforms.pgp import set_credential_store
set_credential_store(my_credential_store)
The credential store must implement async def get(name: str, tenant_id: str) -> str.
Output file name¶
After decryption, the .pgp, .gpg, or .asc extension is stripped from the file name so downstream parsers see the underlying format. payments.csv.pgp becomes payments.csv; report.json.gpg becomes report.json.
Decryption failures¶
All decryption errors raise CryptoError with a machine-readable code:
code |
Cause |
|---|---|
pgp_unavailable |
pgpy not installed |
no_decryption_credential |
Template has no decryption_credential |
credential_fetch_failed |
Credential store returned an error |
invalid_private_key |
Armored key could not be parsed |
pgp_parse_error |
File is not a valid PGP message |
pgp_decrypt_failed |
Wrong key or bad passphrase |
When CryptoError is raised, the runner marks the run as failed, creates an ImportFileFailure, and uploads the original (encrypted) file for diagnosis.
Detached signature verification¶
Signature verification ensures a file was signed with a trusted private key before any processing occurs. It runs before the transform chain so that the file is verified in its original form (still encrypted/compressed if applicable).
Template configuration¶
template = ImportTemplate(
id="bank-payments",
format="csv",
crypto=CryptoConfig(
require_signature=True,
signature_public_key_credential="payments_pgp_public_key",
signature_file_pattern="{file}.sig", # default
),
)
signature_public_key_credential must hold an ASCII-armored PGP public key.
Signature file pattern¶
The signature file must be discoverable alongside the data file. The signature_file_pattern supports two placeholders:
| Placeholder | Resolves to |
|---|---|
{file} |
Full file name, e.g. payments.csv.gz |
{file_stem} |
Name without final extension, e.g. payments.csv |
Examples:
# Default: payments.csv.gz → looks for payments.csv.gz.sig
signature_file_pattern = "{file}.sig"
# Detached sig alongside compressed file: payments.csv.gz → payments.csv.sig
signature_file_pattern = "{file_stem}.sig"
# GPG armor format
signature_file_pattern = "{file}.asc"
The verifier looks for the signature file via the source's open() method (so it works with SFTP, S3, Azure, etc.) and falls back to the local filesystem.
Verification failures¶
code |
Cause |
|---|---|
no_signature_credential |
require_signature=True but no signature_public_key_credential |
signature_missing |
Signature file not found at the resolved path |
pgp_unavailable |
pgpy not installed |
credential_fetch_failed |
Credential store returned an error |
invalid_public_key |
Armored public key could not be parsed |
signature_invalid |
Cryptographic verification failed |
Other CryptoConfig fields¶
| Field | Default | Purpose |
|---|---|---|
decryption_credential |
None |
PGP private key credential name |
require_signature |
False |
Whether a detached signature is required |
signature_public_key_credential |
None |
PGP public key credential name |
signature_file_pattern |
{file}.sig |
Pattern for resolving the signature file |
zip_entry_regex |
None |
If set, only ZIP entries matching this regex are extracted |
max_decompression_ratio |
50.0 |
Maximum allowed decompression ratio (zip-bomb protection) |
Selective ZIP extraction¶
When a ZIP archive contains multiple entries, zip_entry_regex limits which entries are extracted and processed:
Zip-bomb protection¶
max_decompression_ratio limits how much a compressed file may expand. The default of 50.0 means a 1 MB compressed file may decompress to at most 50 MB. Files that exceed this ratio raise CryptoError(code="decompression_ratio_exceeded").
Temporary file security¶
Decrypted content is written to a temporary file with permissions 0o600 (owner read/write only) using a cef-import- prefix. The runner tracks all temporary files per job and deletes them in a finally block regardless of success or failure.
Temp files are created in the system's default temp directory (tempfile.gettempdir()). On Linux this is typically /tmp; consider mounting a tmpfs for additional isolation in containerised deployments.