---
title: "End-to-end encryption: S/MIME authority, OpenPGP and WKD, zero access, sealed mail"
url: https://www.hmailserver.co.uk/topic/28260/end-to-end-encryption-s-mime-authority-openpgp-and-wkd-zero-access-sealed-mail
author: "Progressiverobot"
published: 2026-09-27T19:42:13.731Z
updated: 2026-09-27T19:42:13.731Z
category: "TLS, certificates & deliverability"
tags: ["security","webmail","portal","certificates","limitations"]
site: hMailServer Community Forum
---

# End-to-end encryption: S/MIME authority, OpenPGP and WKD, zero access, sealed mail

*Written for 6.3.4. hMailServer 6.3.7 (17 October 2026) changes the anti-virus action that removes attachments (Remove all attachments and deliver the text in 6.3.7's Control Panel, Delete infected attachments only in 6.3.6's): an S/MIME message signed as one part, which is opaque but not encrypted, is now replaced by the action's note when the scanner finds something in it, where every earlier version delivered it as it arrived, and a message the server cannot read is deleted, its recipients told; an encrypted message is still delivered as it is (work item 92). See [the 6.3.7 announcement](/topic/28277). In hMailServer 6.3.8 (2 November 2026) the web services listener refuses an address an auto-ban or an IP range bans (work item 105), so an IP range of your own that allows some addresses none of SMTP, POP3 and IMAP now closes that listener to them, and with it the Web Key Directory: check that the clients that look keys up there do not come from such a range. See [the 6.3.8 announcement](/topic/28278).*

This topic is about encrypting the message rather than the connection, so that the person it is for can read it and nobody on the way can, this server included. S/MIME in the webmail has been there since 6.3.2; everything else here is new in 6.3.4. The design rule running through all of it is that **this server holds nothing that decrypts a person's mail**: keys are made in the browser and kept wrapped under a passphrase or password the server never sees. The one thing to know before you allow any of it is the cost of that rule: **nothing on this server can recover mail encrypted to a key that was lost.** There is no escrow.

### Which one you want

| You want | Use |
|---|---|
| S/MIME inside the organisation without buying a certificate per person | A domain's **S/MIME authority** |
| Correspondents who use GnuPG, Thunderbird, Proton or Mailvelope | **OpenPGP in the webmail**, and the **Web Key Directory** |
| To write privately to somebody who has no key at all | A **password-protected message** |
| A mailbox the administrator cannot read | **Zero access** |
| Everything to one partner organisation encrypted, whatever the sender's client did | **Gateway encryption**, on a remote domain policy |
| A stolen disk or backup to hold nothing readable, while the mailbox works as before | **[Mailbox encryption at rest](/topic/28233)**, which the server can read and so is not in this topic |

All of the browser cryptography needs a page opened over **HTTPS**: a browser withholds `crypto.subtle` from any other, and the page says so instead of offering Sign and Encrypt.

### S/MIME with your own authority

*Since 6.3.4 (schema 6095).* A domain may have its own S/MIME authority: a root certificate the domain owns, which certifies the keys its accounts make in the webmail, publishes them for colleagues to look up, and says what it has revoked in a signed CRL. Nothing is issued until an administrator creates one.

**Who holds what.** The person's private key never leaves the browser. The webmail makes the key pair, builds a PKCS#10 request, wraps the private key under the account password, and sends only the request. The authority's own key is on the server, because it signs there, and it is protected by the platform's secret store:

| | Where the authority's key opens |
|---|---|
| Windows | DPAPI with the machine's key. It opens on this machine and nowhere else, and on this machine to any process that asks |
| Linux | The stored-secret key file, `<DataFolder>/.hmailserver-secret-key` (AES-256-GCM, mode 0600). It opens wherever that file is |

It never falls back to the legacy scheme. When the store is unavailable the authority is not created (`503`).

**Creating it.** In the Control Deck, open the domain and find **S/MIME authority**, or `POST /api/v1/domains/{domain}/smime-ca` with `{"key": "p256"}` or `"rsa3072"`. The root is self-signed, CA:TRUE with pathlen:0, valid ten years, with a critical name constraint permitting the domain's own addresses and nothing else. P-256 is the default. **The Control Panel shows nothing of it, and there is no COM surface.**

**Enrolment.** A person finds **Get a certificate from {domain}** under **Settings → Security → S/MIME**.

1. They pick the key type. **RSA 2048** is the default and the one to choose. A P-256 certificate from this page signs, and neither this webmail nor a colleague's can encrypt to it.
2. The page makes the key pair and the request, and asks for the account password that wraps the private key before anything is sent.
3. `POST /api/v1/me/smime/enroll` is made from the account's own sign-in. An app password and an administrator's support session are refused with `403`.
4. The server checks the request. It must name exactly one address, and that address must be the account's own or an active alias of its own domain that points at it.
5. The certificate is issued **pending**. When it arrives in the account's own store it is confirmed, and only then is it answered to colleagues and the address's earlier certificates revoked as *superseded*.

The limits are five enrolments a day (`429`) and ten valid certificates at once (`409`). A person with two devices ticks **Keep my other certificates for this address** on the second.

**What is issued.** Valid one year. With `smime_authority_base_url` empty, which is the default, a certificate carries no CRL distribution point. What that costs: Outlook and Thunderbird cannot fetch the CRL from the certificate, so a revocation reaches them only when they have been given the CRL another way. This webmail does not use the CRL at all; it reads the authority's own record.

**Publication and trust.** The root and the CRL are served over plain HTTP by the web services listener (`WebServicesHttpPort`), at `/smime-ca/{domain}.pem`, `.cer` and `.crl`. A signature verifies in Outlook, Thunderbird, Apple Mail or a phone only once **the root is installed on the clients**, by group policy, a device-management profile or by hand. This webmail trusts the domain's authority by itself. A colleague's certificate is looked up with `GET /api/v1/me/directory/{address}/smime`, which the webmail asks on its own when **Encrypt** is ticked.

**Moving and restoring.** The authority is in the configuration backup, but its key opens only where its envelope opens. Restored anywhere else, the Deck shows a warning, enrolment is refused with `503`, its CRL is not served, and the authority **must be rotated** there. Rotation makes a new root and retires the old one without deleting it, and clients must install the new root beside the old.

**What it is not.** Not Active Directory Certificate Services. No OCSP. No key escrow: a forgotten password that the key was wrapped under leaves the mail encrypted to that key unreadable, by anybody, for ever. No elliptic-curve encryption in the webmail. One address per certificate. No automatic renewal: a person enrols again before the year is out.

### OpenPGP: the half in the directory and the half in the browser

| | The directory | The page |
|---|---|---|
| Where it runs | On the server | In the person's browser |
| What it holds | **Public** keys only | Own keys, the secret half encrypted under a passphrase, and correspondents' public keys |
| What cryptography it does | None at all | All of it: signing, encrypting, decrypting, verifying, in PGP/MIME |
| Table and schema | `hm_pgpkeys`, schema 6050 | `hm_pgpring`, schema 6080; the trust columns 6081 |
| State as shipped | Three switches, all off | `portal_enabled` on |

In neither half does the server sign, verify, encrypt or decrypt a message. It makes an OpenPGP message itself in only two places further down, a delivery to a zero-access mailbox and gateway encryption to a partner's key; there it encrypts, never signs or decrypts, and verifies only the binding of the subkey it encrypts to.

**The directory** publishes keys at a URL derived from the address, so that anybody who has the address can find the key. `<hash>` is the local part, lower-cased, SHA-1 hashed and z-base-32 encoded. Both methods are served:

| Method | The key |
|---|---|
| advanced | `https://openpgpkey.<domain>/.well-known/openpgpkey/<domain>/hu/<hash>` |
| direct | `https://<domain>/.well-known/openpgpkey/hu/<hash>` |

Three switches have to be on before one key is served:

1. **The server**: `OpenPGPWebKeyDirectoryEnabled`, set with `hmctl settings set openpgp --set web_key_directory_enabled=true` or `PUT /api/v1/settings/openpgp`.
2. **The domain**: `openpgp_publish` on the domain.
3. **The account**: `openpgp_publish` on the account. This one is the person's own, through `PUT /api/v1/me/openpgp/publish`.

The web services listener has to exist (`WebServicesHttpsPort`), and the advanced method, which clients try first, needs an address record for `openpgpkey.<domain>` and a certificate valid for it. **Keys are served over HTTPS only.** Every refusal is the same `404`, whether the domain is unknown, a switch is off, or the account has no key, so that the directory cannot be used to ask whether somebody has an account.

Put a key in with `hmctl openpgp store ann@example.com ann.asc --publish`, and check it as a correspondent's client will:

```text
gpg --locate-keys --auto-key-locate clear,wkd ann@example.com
```

An upload carrying a **secret**-key packet is refused; so are a version 3 key, a key with no user id or no self-signature, a key past `OpenPGPKeyMaximumBytes` (65536), and a key whose user ids do not carry the account's own address. An account may hold `OpenPGPKeysPerAccount` keys (8). A signature on an uploaded key is not verified cryptographically: what makes a key in this directory the right one is that its owner or an administrator put it there. The desktop Control Panel has nothing for the directory yet.

**The page** is `/portal-openpgp.js`, written on the Web Crypto API with no library vendored.

- **Keys.** Curve25519 (the default) or RSA 3072 or 4096, made in the browser. The secret key is stored as OpenPGP writes one, under the passphrase, so the bytes the server holds are bytes it cannot open, and the same bytes import into GnuPG. A revocation certificate is made at the same moment and saved to the device.
- **Autocrypt** Level 1, both ways.
- **Finding a key.** `GET /api/v1/me/openpgp/lookup?address=` asks the correspondent's own Web Key Directory (`lookup_wkd`, on) and then a keyserver (`lookup_keyserver`, **off** by default; `keyserver_url` is `https://keys.openpgp.org`). Nothing is stored by the lookup.
- **Trust made visible.** A different key arriving for an address the ring already holds a key for is stored as a change. Until the person accepts it, the page does not encrypt to it. A key can be marked *verified in person*, and the mark is never inherited by a key that replaces it.
- **Who may write.** Every write to the ring is refused to an app password and to an administrator's support session.

**Not implemented, and refused by name:** version 5 and 6 keys and AEAD; DSA and ElGamal keys and the NIST and Brainpool curves; smartcards and hardware tokens; the web of trust; a draft or a scheduled send that is to be sealed. Curve25519 needs Chrome 137, Firefox 130 or Safari 17 or later. Both directions are proved against GnuPG 2.4 by `build/check-portal-openpgp.js`, which GitLab CI runs on every push and every merge request.

**A password-protected message** (schema 6082) is for the correspondent with no key. The page encrypts the text and attachments to a password as one OpenPGP message, and what goes out by SMTP is a short notice with a link to `/portal/protected/<token>`, where the recipient's browser decrypts it. The link is the whole access control. It needs `portal_enabled` on and a public address for the links (the `confidential_base_url` chain [the messages that expire](/topic/28254) use), and is offered only over HTTPS. A message lives `protected_message_days` (30 by default). The ceilings are 25 MB a message and 250 MB an account. It does not promise that the recipient cannot copy what they read.

### Zero-access mailboxes

*Since 6.3.4 (schema 6083).* A mailbox this server holds and cannot read. An administrator allows it server-wide once (`zero_access_allowed`, under `/api/v1/settings/openpgp`, off by default); the **person** then switches it on for their own account on the webmail's Security page. From then on every message delivered to the account is encrypted to the account's own OpenPGP key - after the spam and virus filters, the rules and the Sieve script have had the text - and stored as PGP/MIME with the headers in the clear. An administrator sees the switch read-only everywhere.

**What it costs.** The webmail refuses to switch until the recovery key has been typed back and the sentence about a lost password has been ticked.

| Cost | What it means |
|---|---|
| The administrator cannot read the mail | The message source and the account export answer `403` |
| Neither the administrator nor the server can search it | The index and IMAP SEARCH read the headers, never the text. A word in the subject is found; a word in the body is not |
| Nobody can recover it | A forgotten password without the recovery key is the mail lost, for ever |
| IMAP and POP see ciphertext | Valid PGP/MIME that a client holding the key opens. Until then the account is a webmail account |
| The plaintext was on the disk | From acceptance until the seal. The account's copy is overwritten with zeros; whether the bytes are gone is the filesystem's decision |
| Only what is delivered is sealed, and the Sent copy | A draft, and anything a client uploads with IMAP APPEND, is stored as it is |
| A journal rule sees the plaintext | Journaling runs at acceptance, before delivery |
| The filters have read it | A forward goes on in the clear. A rule that files into a public folder files the plaintext there. Quarantined mail is held in the clear |
| Switching off decrypts nothing | Mail that arrived while it was on stays encrypted to the key |

**The headers the server kept.** `From`, `To`, `Cc`, `Date`, `Subject`, `Message-ID`, the `Received` trace and the rest stay in the clear, so that the listing, the threading and the spam folder work without the key. The server adds `X-hMailServer-Zero-Access: sealed on delivery`.

**The key, in two sealed copies.** `hm_zeroaccess` holds two copies of the secret key, both sealed in the browser:

- one under a passphrase the page derives from the account password with **Argon2id**, three passes over 64 MiB in four lanes. It was measured at 1.6 seconds in Node 24 on the development machine; a phone is expected to take several seconds, and it was **not measured on one**;
- one under the **recovery key**: 30 random octets shown once as eight groups of six base32 characters.

A key that carries an expiry is refused at the switch-on.

**When a seal fails, the delivery is refused** rather than stored in the clear: the sender is told with `4.3.0` and the error log carries `HM7070`.

**Passwords.** A change through the webmail carries the key sealed again under the new password, and the server refuses a change without it (`409`, `zero_access_rewrap`). A password an administrator sets cannot re-wrap; at the next sign-in the page asks for the previous password or the recovery key.

**It is not encryption at rest.** Mailbox encryption at rest stores mail under a key the server holds while the person is signed in. The two are never on one mailbox: each switch is refused where the other is on.

**Not built:** a local bridge for IMAP or POP clients; search over the decrypted text in the browser; encryption of mail already in the mailbox when the switch is thrown.

### What the server does with a sealed message

A **sealed** message is one signed or encrypted end to end, which must leave this server byte for byte as it arrived. An **opaque** one is a sealed message the server cannot read at all. A `multipart/signed` message is sealed but not opaque: its text is in the clear.

**Opaque is decided from the body, never from the label**, or every check that stands aside for an encrypted message would be one header away from being switched off by anyone sending mail. The body must be S/MIME whose one part begins a CMS object (enveloped-data, authenticated-enveloped-data or signed-data), or PGP/MIME of exactly two parts: `Version: 1`, then one armoured OpenPGP message. None of what follows is a setting.

| Part of the server | What it does with an opaque message |
|---|---|
| Blocked-attachment list | Never rewrites it. One application log line says the list was not applied. In 6.3.3 and earlier, blocking `*.asc` or `*.p7m` destroyed the ciphertext without a log line |
| [Virus scanner](/topic/28250) | Scans it, and logs that its content `COULD NOT BE EXAMINED for viruses`. It is not treated as a scanner failure, so `AVFailAction=1` does not hold it. If mail the server cannot scan must not be delivered, use a rule or a Sieve script on `body :raw` |
| *Delete attachments* action (*Remove all attachments and deliver the text* since 6.3.7) | Never strips an encrypted message. Since 6.3.7 an S/MIME message signed as one part - `application/pkcs7-mime` holding signed-data, opaque but not encrypted - is replaced by the action's note when the scanner finds something in it, where it was delivered as it arrived (work item 92). Set the action to delete the message if such mail must not be delivered |
| [Rules](/topic/28268) | A body criterion does not match, whichever way it is phrased, with one log line per rule per message: under AND the rule does not fire; under OR its other criteria still count. To catch encrypted mail, test `Content-Type` for `multipart/encrypted` or `application/pkcs7-mime` with a custom-header criterion |
| Sieve | The evaluation is unchanged, as RFC 5173 requires; a log line is new. Use `body :raw` to recognise such a message |
| Search and the index | Only the header is indexed |
| Content spam scoring | Sees nothing in it, so encrypted spam is scored more leniently. SPF, DKIM, DMARC and DNSBL are unaffected |

A message only *labelled* encrypted, whose parts are readable, is blocked, stripped, filtered and searched like any other. In [SealedMail.md](https://gitlab.com/hmailserver/hmailserver/-/blob/v6.3.4/hmailserver/docs/SealedMail.md)'s words: a message that arrives encrypted is not scanned, and your mail client, and you, are the last check.

### Gateway encryption, briefly

*Since 6.3.4 (schema 6094).* On a [remote domain policy](/topic/28257), *Encrypt mail to this domain at this server* encrypts every message to one partner, here, to the partner's own gateway key. It encrypts at the server, as a delivery to a zero-access mailbox does, and the server holds no private key for it. **The Subject is not hidden.** When a message cannot be encrypted the default is to refuse it with `5.7.10`. It does not sign, look keys up, or decrypt replies.

### Where to read more

- Operator documents at the v6.3.4 tag: [SmimeAuthority.md](https://gitlab.com/hmailserver/hmailserver/-/blob/v6.3.4/hmailserver/docs/SmimeAuthority.md), [OpenPGP.md](https://gitlab.com/hmailserver/hmailserver/-/blob/v6.3.4/hmailserver/docs/OpenPGP.md), [ZeroAccess.md](https://gitlab.com/hmailserver/hmailserver/-/blob/v6.3.4/hmailserver/docs/ZeroAccess.md), [SealedMail.md](https://gitlab.com/hmailserver/hmailserver/-/blob/v6.3.4/hmailserver/docs/SealedMail.md) and [RemoteDomains.md](https://gitlab.com/hmailserver/hmailserver/-/blob/v6.3.4/hmailserver/docs/RemoteDomains.md).
- Wiki: [Encryption for People](https://gitlab.com/hmailserver/hmailserver/-/wikis/Encryption-for-People) and [Encryption and Certificates](https://gitlab.com/hmailserver/hmailserver/-/wikis/Encryption-and-Certificates).
- Public documentation: [36.6 The portal: what a user gets](https://www.progressiverobot.com/hmailserver-documentation/#366-the-portal-what-a-user-gets).
- On this forum: [The webmail portal at /portal](/topic/28191), [The Control Deck](/topic/28192), [Moving to new hardware](/topic/28194) before you carry an authority's key to another machine, and the rest of [this category](/category/18).
