Skip to content

feat(smime): add S/MIME support via companion-app architecture#11033

Open
christine-ciphermail wants to merge 2 commits into
thunderbird:mainfrom
christine-ciphermail:upstream-smime-pr
Open

feat(smime): add S/MIME support via companion-app architecture#11033
christine-ciphermail wants to merge 2 commits into
thunderbird:mainfrom
christine-ciphermail:upstream-smime-pr

Conversation

@christine-ciphermail

Copy link
Copy Markdown

Summary

Adds end-to-end S/MIME support to Thunderbird for Android via a companion-app architecture, mirroring the existing OpenPGP integration: key material and cryptographic operations live in a separate provider app, and Thunderbird talks to it over a stable AIDL API.

A reference provider implementation (CipherMail for Android) is the first consumer of this API, but any S/MIME provider can implement it.

What's included

New module plugins/smime-api/ — versioned AIDL + Parcelables + client helper. Pluggable across providers, mirrored in the CipherMail companion repo. Independent licensing under Apache-2.0.

Integration in legacy core / UI:

  • Account-level Sign/Encrypt toggles, with per-account defaults (account settings → S/MIME)
  • Compose: per-message Sign/Encrypt checkboxes; encrypt-implies-sign enforced; lock/sign icons in the recipient bar reflect provider's certificate availability check
  • Message view: separate "signed" / "encrypted" status icons and an "Open in provider" action for messages Thunderbird can't render inline
  • S/MIME state persisted separately from the legacy crypto-enabled flag so a message is never sent silently unencrypted when the provider is unavailable

Safety behaviour:

  • If the provider isn't installed or returns USER_INTERACTION_REQUIRED (e.g. provider keystore is locked), the compose flow pauses and surfaces the action to the user — the message is preserved, not lost
  • Drafts are offered a folder assignment when the composer is closed before a save completes

Documentation:

  • docs/adr/0009-smime-companion-app-architecture.md — architectural decision and rationale (why companion app vs. built-in)
  • docs/security/smime-companion-threat-model.md — threat model for the inter-process boundary
  • docs/developer/writing-smime-provider.md — guide for implementers of the API
  • docs/user-guide/setup/enabling-smime.md — end-user setup walkthrough

Why companion-app and not built-in?

Same reasons OpenPGP went this route in K-9 Mail / Thunderbird for Android: cryptographic key material, certificate stores, PIN unlock flows, and smartcard support are non-trivial concerns that benefit from living in a single, dedicated, hardened process. Thunderbird stays a mail client; providers stay providers. See ADR-0009 for the full discussion.

Test plan

  • Build cleanly on AGP 9 / Gradle 9 / JDK 21
  • Existing unit tests pass; new tests cover RecipientPresenter.onSmimeCertCheckResult branches
  • Manual: enable S/MIME on an account, sign-only, encrypt+sign, send/receive between two devices using CipherMail provider
  • Manual: uninstall provider, confirm error path (no silent unencrypted send, message preserved)
  • Manual: provider keystore locked → USER_INTERACTION_REQUIRED flow opens the provider's unlock UI, retry succeeds

Notes

  • No breaking changes to existing behaviour for users who don't enable S/MIME on an account
  • API module is versioned independently (plugins/smime-api/CHANGELOG.md) so providers and Thunderbird can evolve at their own pace

@github-actions

github-actions Bot commented May 21, 2026

Copy link
Copy Markdown
Contributor

Validation Passed: All report and feature-flag labels are correctly set.

@christine-ciphermail

Copy link
Copy Markdown
Author

Please apply report: include

I can't seem to add a lablelmyself.

@christine-ciphermail

Copy link
Copy Markdown
Author

How do I add a label?

Adds support for delegating S/MIME crypto operations to a separate
companion app over an AIDL service, paralleling the existing OpenPGP /
OpenKeychain integration. The reference provider is CipherMail
(com.ciphermail.android); other providers can implement the same API.

API surface (new module `plugins/smime-api/smime-api/`)
  - ISmimeService AIDL — execute(Intent, ParcelFileDescriptor, int) +
    createOutputPipe(int) for streaming bulk MIME data.
  - SmimeApi helper (sync + async execution wrappers, pipe management).
  - SmimeServiceConnection (bind lifecycle helper).
  - Parcelables: SmimeError, SmimeSignatureResult, SmimeDecryptionResult,
    SmimeCertificateInfo. All carry PARCELABLE_VERSION = 1.
  - Actions: CHECK_PERMISSION, DECRYPT_VERIFY, SIGN_AND_ENCRYPT,
    GET_CERTIFICATES, IMPORT_CERTIFICATE.

Receive-side integration
  - SmimeCryptoHelper (parallels MessageCryptoHelper for OpenPGP):
    detects S/MIME parts, binds to the provider, dispatches DECRYPT_VERIFY,
    surfaces RESULT_CODE_USER_INTERACTION_REQUIRED via PendingIntent so
    the host can launch the provider's passphrase dialog.
  - MessageCryptoStructureDetector.isSmimePart and helpers — detect
    application/pkcs7-mime and PKCS#7 multipart/signed.
  - CryptoResultAnnotation: new S/MIME fields and createSmime* factories.
  - MessageCryptoDisplayStatus: S/MIME signature/encryption mappings to
    the existing display-status badges.
  - MessageLoaderHelper, MessageCryptoPresenter, MessageViewInfoExtractor
    wired through.

Send-side integration
  - SmimeMessageBuilder (parallels PgpMessageBuilder): binds to the
    provider on a background thread, calls SIGN_AND_ENCRYPT, returns the
    wrapped MIME message for SMTP transport. Drafts bypass crypto.
  - MessageCompose: S/MIME branch in createMessageBuilder(), checked
    before PGP.
  - RecipientPresenter.asyncUpdateSmimeCertStatus: calls
    GET_CERTIFICATES on recipient changes; drives the compose lock-icon
    state (green = all certs present, red = missing).

Per-account configuration
  - LegacyAccount / LegacyAccountDto: smimeProvider field +
    isSmimeProviderConfigured.
  - LegacyAccountStorageHandler + DefaultLegacyAccountDataMapper:
    persist smimeProvider.
  - AccountSettingsFragment: S/MIME PreferenceScreen + provider picker.
  - SmimeAppSelectDialog: enumerates installed providers via
    SmimeApi.SERVICE_INTENT and lets the user choose. Binding always
    uses setPackage(account.smimeProvider) to avoid intent-filter
    interception.

Manifest plumbing
  - app-k9mail and app-thunderbird AndroidManifest: <queries> for
    ISmimeService discovery on Android 11+.
  - legacy/ui/legacy AndroidManifest: register SmimeAppSelectDialog.

Cross-process passphrase handshake
  - When the provider's keystore is locked it returns
    RESULT_CODE_USER_INTERACTION_REQUIRED with an immutable PendingIntent
    for its passphrase activity. Thunderbird launches via
    startIntentSenderForResult and retries on RESULT_OK. No inline
    prompting, no IPC timeouts.

Send/receive UX and per-identity signing
  - User-controllable Sign / Encrypt toggles with per-account defaults;
    S/MIME-enabled is persisted separately and a message is never sent
    silently unencrypted.
  - Encrypt-implies-sign enforced (no encrypt without a signature).
  - Per-identity signing: EXTRA_FROM carries the composing account's
    address on SIGN_AND_ENCRYPT so the provider signs with the matching
    certificate.
  - Separate signed / encrypted status icons and an "open in provider"
    action to view any encrypted or signed message in CipherMail.
  - Provider-unavailable handling: never lose a message; gate composer
    close on save completion; offer Drafts-folder assignment.
  - Send failures surface in a dismissible dialog rather than a Toast.
  - Set the intent extras class loader before reading Parcelable extras.
  - Test: RecipientPresenter.onSmimeCertCheckResult result branches.
…rovider guide

Adds end-to-end documentation for the S/MIME companion integration:

Library docs (`plugins/smime-api/`)
  - README — client-side tutorial (bind, execute, result + PendingIntent
    flow) and action-by-action reference. Mirrors openpgp-api-lib/README.
  - CHANGELOG — Version 1 inventory of actions / extras / Parcelables.
  - LICENSE — Apache 2.0 (matches openpgp-api-lib).

mdbook docs
  - architecture/adr/0009 — Companion App + AIDL Service for S/MIME:
    decision record covering the three alternatives (in-process library,
    embedded crypto core, companion app) and why the companion-app model
    was chosen. Includes a Mermaid sequence diagram for the sign+encrypt
    + passphrase-unlock flow.
  - security/smime-companion-threat-model — STRIDE pass on the IPC trust
    boundary: provider discovery, binding, request/result tampering,
    PendingIntent hijacking, pipe DoS, cert-lookup honesty. Risks ranked,
    residual-risk notes for the two trade-offs inherent to the model.
  - user-guide/setup/enabling-smime — end-user walkthrough (install
    provider, set keystore passphrase, import certificate, enable
    S/MIME on the account, first send/receive). Includes a compose
    lock-icon state reference and a translator's inventory of the new
    string resources.
  - developer/writing-smime-provider — normative spec for implementing
    an alternative S/MIME provider: manifest declarations, AIDL contract,
    per-action behaviour and edge cases, the user-interaction handshake,
    EXTRA_API_VERSION negotiation, EXTRA_FROM for per-identity signing,
    security obligations (caller identity, no outbound network,
    trust-signal honesty), a testing checklist.
  - SUMMARY.md — all four new documents wired into the mdbook TOC.
@wmontwe wmontwe added the report: include Include changes in user-facing reports. label Jun 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

report: include Include changes in user-facing reports.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants