Add OpenPGP and S/MIME signing of outgoing mail

mail.pgp.signing.type selects NONE (default), OPENPGP or SMIME, with one
key/certificate per From address.

- OPENPGP: plaintext copies become PGP/MIME multipart/signed (RFC 3156 s5,
  detached signature over the exact content entity bytes); encrypted copies
  are signed inside the encryption (combined method)
- SMIME: plaintext copies become S/MIME multipart/signed (RFC 8551, via
  Bouncy Castle bcjmail); encrypted copies are S/MIME-signed, then
  PGP/MIME-encrypted
- signed bodies are attached as raw bytes so they reach the wire unchanged;
  shared MIME helpers move to MimeEntities
- keys are validated at startup (signing capability, address match,
  validity, passphrase / key-certificate match)
- mail.pgp.signing.required refuses mail from addresses without a key
  (MailSigningException); RELAY mode signs before handing over
- SendReport.signature(), metric tag signature, refused{cause}
- core test-jar shares key/certificate and verification helpers
- end-to-end tests verify signatures on the bytes received over SMTP

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G71TQ5jDETVwdUJxGAwaiT
This commit is contained in:
Claude committed 2026-10-15 09:49:45 +00:00
1 parent fb52ee2c27
commit 7c2fb0142e
32 files changed
+1653 -133

No files matched your search

+31 -1
View File
@@ -14,7 +14,7 @@ Requires JDK 25 or newer; the Maven Wrapper (`./mvnw`) downloads Maven 3.9.11. B
| Module | Contents |
|---|---|
| `mailsender-pgp-core` | WKD key discovery, caching, pinned keys, certificate validation, OpenPGP encryption, PGP/MIME (no Spring) |
| `mailsender-pgp-core` | WKD key discovery, caching, pinned keys, certificate validation, OpenPGP encryption, PGP/MIME, OpenPGP and S/MIME signing (no Spring) |
| `mailsender-pgp-spring-boot-autoconfigure` | `mail.pgp.*` properties and beans |
| `mailsender-pgp-spring-boot-starter` | dependency bundle for applications |
| `mailsender-demo` | sample app that sends test mails |
@@ -75,6 +75,36 @@ Map keys that are addresses need the `[...]` brackets. See [docs/PLAN.md](docs/P
- With Actuator, expose `pgpkeys` to inspect an address (`GET /actuator/pgpkeys/{email}`) or evict cached keys
(`DELETE /actuator/pgpkeys/{email}`).
## Signing
Outgoing mail can be signed with the From address's OpenPGP key or S/MIME certificate:
```yaml
mail:
pgp:
signing:
type: OPENPGP # NONE (default) | OPENPGP | SMIME
required: false # true: refuse mail from an address without a signing key
openpgp:
"[noreply@example.com]":
key: file:/etc/mail/noreply.sec.asc # secret key
passphrase: ${SIGNING_PASSPHRASE}
smime:
"[noreply@example.com]":
keystore: file:/etc/mail/noreply.p12 # PKCS#12: private key + certificate chain
password: ${SMIME_PASSWORD}
```
| | Plaintext copies | Encrypted copies |
|---|---|---|
| `OPENPGP` | PGP/MIME `multipart/signed` (RFC 3156) | signed inside the encryption |
| `SMIME` | S/MIME `multipart/signed` (RFC 8551) | S/MIME-signed, then PGP/MIME-encrypted |
Keys are checked at startup: the key/certificate must belong to the address, be able to sign and be currently valid,
and the passphrase must work. `SendReport.signature()` says how a message was signed. In RELAY mode the message is
signed before it is handed to the gateway. Do not enable `mail.smtp.allow8bitmime` together with signing: re-encoding
the body in transit invalidates signatures.
## Demo app
The demo app sends `DEMO_COUNT` test mails (default: 1 to `test@thinhha.pro`) through `SecureMailSender` and logs