Check the S/MIME half against a real server, instead of assuming it

The section offered "an OpenPGP public key or an S/MIME certificate" and
only the first half had ever been tried. Every probe behind it used
OpenPGP keys, and every message the registry returns names OpenPGP --
including for input that is not OpenPGP at all -- so the server reads as
though OpenPGP were the only format it knows. Shipping the claim on that
evidence would have been a guess dressed as a feature, which is the one
thing this section is written not to do.

It holds. Confirmed live on 0.16.20 (2026-09-05) with a self-signed
X.509 certificate carrying emailProtection and an email: SAN:
registered, read back, destroyed. And Stalwart parses it as seriously as
it parses OpenPGP -- a malformed certificate is refused by a decoder of
its own, "Failed to decode X509 certificate: BER decoding error:
Expected Tag { class: Universal, value: 16 } tag…", which is a third
rejection wording and the reason the S/MIME half is real rather than
decorative. The mock now returns it for a certificate, so the branch
exists somewhere a client can meet it.

One thing found on the way: expiresAt is the registry's field and is not
derived from the key. A certificate valid for a year registers with
expiresAt null, so the card says "No expiry set" about a credential that
does expire. Left as it is, deliberately: reading the real date means
parsing the certificate, which is the second opinion this section
refuses to offer, and a date extracted here would disagree with the
server's own field the moment the two ever differed. What the row
reports is what the registry holds, and KNOWN-ISSUES says so.
This commit is contained in:
2026-09-05 01:03:52 -07:00
parent e93d42d27e
commit 6a467d9bc4
3 changed files with 30 additions and 11 deletions
+17 -11
View File
@@ -1012,7 +1012,7 @@ const handlers: Record<string, Handler> = {
const destroyed: string[] = [];
for (const [cid, obj] of Object.entries((a.create as Obj) ?? {})) {
const o = obj as Obj;
const complaint = pgpComplaint(String(o.key ?? ""));
const complaint = keyComplaint(String(o.key ?? ""));
if (complaint) {
notCreated[cid] = { type: "invalidProperties", properties: ["key"], description: complaint };
continue;
@@ -1278,23 +1278,29 @@ const handlers: Record<string, Handler> = {
* that only ever saw "invalid key" would show something less useful than what
* the server was already offering.
*
* The two are worth keeping apart, because they are different problems and the
* second is the one a real person hits. A block that will not parse is usually
* a bad copy and paste. A block that parses and is still refused is a key that
* cannot encrypt -- `gpg --quick-generate-key` makes a sign-and-certify key by
* default, and exporting that gets you "Could not find any suitable keys"
* however carefully it was pasted.
* There are three, and they are worth keeping apart because they are different
* problems. A block that will not parse is usually a bad copy and paste. A
* block that parses and is still refused is a key that cannot encrypt --
* `gpg --quick-generate-key` makes a sign-and-certify key by default, and
* exporting that gets you "Could not find any suitable keys" however carefully
* it was pasted. And a certificate gets its own decoder and its own complaint:
* Stalwart parses X.509 as seriously as it parses OpenPGP, which is the thing
* that makes the S/MIME half of this section real rather than decorative.
*/
function pgpComplaint(key: string): string | null {
function keyComplaint(key: string): string | null {
const k = key.trim();
if (!k) return "Failed to decode OpenPGP public key: no key data.";
const pgp = k.startsWith("-----BEGIN PGP PUBLIC KEY BLOCK-----") && k.includes("-----END PGP PUBLIC KEY BLOCK-----");
const x509 = k.startsWith("-----BEGIN CERTIFICATE-----") && k.includes("-----END CERTIFICATE-----");
if (!pgp && !x509) return "Failed to decode OpenPGP public key: Malformed packet: Malformed CTB: MSB of ptag not set.";
const body = k.split(/\r?\n/).filter((l) => l && !l.startsWith("-----") && !l.startsWith("=") && !l.includes(":")).join("");
// Enough base64 to be a key rather than a placeholder; the real parser is
// stricter still, which is the point of surfacing its message and not ours.
if (body.length < 64) return "Failed to decode OpenPGP public key: Malformed packet: unexpected EOF.";
// Enough base64 to be a key rather than a placeholder; the real parsers are
// stricter still, which is the point of surfacing their message and not ours.
if (body.length < 64) {
return x509
? "Failed to decode X509 certificate: BER decoding error: Expected Tag { class: Universal, value: 16 } tag, actual tag: Tag { class: Application, value: 14 } (Codec: BER)"
: "Failed to decode OpenPGP public key: Malformed packet: unexpected EOF.";
}
// The mock cannot read a key, so it cannot tell whether one can encrypt.
// A "SIGNONLY" marker anywhere in the block stands in for that, which is
// crude but reachable: the branch has to be reachable from the UI, or