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:
+17
-11
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user