OpenPGP Encryption Provider v1

The Stella Ops crypto-boundary primitive for OpenPGP public-key encryption: an in-process provider that operator-controlled email handoff workflows (such as NIS2 CSIRT fallback delivery) call instead of touching OpenPGP libraries or gpg directly. This contract is for implementers wiring that boundary and for reviewers verifying its fail-closed validation guarantees.

Owner: Security & Cryptography Guild

Runtime provider: src/Cryptography/StellaOps.Cryptography.Plugin.OpenPgp/

Contract name: StellaOps.Cryptography.OpenPgpEncryptionProvider/v1

Provider id: stellaops.openpgp.bouncycastle.v1

Both constants are defined in OpenPgpEncryptionProviderContract (ContractVersion and ProviderId) and are re-declared verbatim in the plugin manifest (plugin.yaml: pluginId == contract name, providerId == provider id).

Provider Surface

The contract is a crypto-boundary library primitive. It exposes no HTTP route, CLI command, OAuth scope, or persistence schema; it is consumed in-process via the IOpenPgpEncryptionProvider interface:

The shipping implementation is BouncyCastleOpenPgpEncryptionProvider, built on BouncyCastle.Cryptography.

Purpose

This contract defines the StellaOps crypto-boundary primitive for OpenPGP public-key encryption used by operator-controlled email handoff workflows such as NIS2 CSIRT fallback delivery. Notify and other product modules must consume this provider boundary instead of calling OpenPGP libraries or shelling out to gpg directly.

Inputs

The request DTO is OpenPgpEncryptionRequest (all fields are required):

Required Validation

The provider fails closed unless all checks pass. Every failure surfaces as an OpenPgpEncryptionException carrying a stable ErrorCode:

Note: when a candidate encryption key fails revocation, expiry, capability, or flag checks, that candidate is skipped and selection continues to the next matching key; only if no candidate survives is openpgp.encryption_key_unavailable thrown (with the collected reasons). The adjacent tests exercise the fingerprint-mismatch and missing-encryption-flag paths via this aggregate error code, and the expired-primary path via openpgp.primary_key_expired.

Error codes

The full set of ErrorCode values emitted by the v1 provider:

Error codeCondition
openpgp.recipient_fingerprint_invalidNormalized fingerprint is not 40 or 64 hex chars.
openpgp.public_key_missingArmoredPublicKey is null/blank.
openpgp.plaintext_emptyPlaintext is empty.
openpgp.literal_filename_missingFileName is null/blank.
openpgp.evaluation_time_missingEvaluationTimeUtc is default.
openpgp.public_key_parse_failedArmored key material could not be parsed.
openpgp.primary_key_revokedMatched primary key is revoked.
openpgp.primary_key_expiredMatched primary key expired at evaluation time.
openpgp.encryption_key_revokedCandidate encryption key is revoked.
openpgp.encryption_key_expiredCandidate encryption key expired at evaluation time.
openpgp.encryption_key_capability_missingCandidate key is not an encryption key.
openpgp.encryption_key_flags_missingCandidate key declares key flags but lacks an encrypt flag.
openpgp.encryption_key_unavailableNo candidate key satisfied all checks.

Output Evidence

Successful encryption returns an OpenPgpEncryptionResult with the following fields:

OpenPGP encryption is intentionally randomized; the ciphertext itself is not byte-stable across runs. The audit-grade deterministic evidence is the sha256:-prefixed digest computed over the exact ciphertext produced for that handoff.

Offline Expectations

Tests must run without network, SMTP, live keyservers, or external gpg. Adjacent coverage generates local test key rings, encrypts through the provider, decrypts with the generated secret key, verifies the integrity packet, and asserts fingerprint mismatch, expiry, and missing encryption-key-flag failures.

Current tests:

Plugin Manifest

The provider ships as a Cryptography plugin. plugin.yaml declares:

Consumers

The NIS2 CSIRT outbound connector consumes this boundary via Nis2OpenPgpEmailEncryptor (src/Notify/__Libraries/StellaOps.Notify.Connectors.NCS/), which resolves operator-controlled key material through an INis2PgpPublicKeyResolver, builds the deterministic handoff JSON, calls IOpenPgpEncryptionProvider.EncryptAsync, and forwards the armored message, sha256:-prefixed digest, contract version, provider id, recipient fingerprint, and encryption-key fingerprint into Nis2CsirtEncryptedPgpEmail. Connectors and other product modules must consume this provider boundary instead of calling OpenPGP libraries or shelling out to gpg directly.