12. Das Secure-Link-Ausweichportal¶
Ende-zu-Ende-Verschlüsselung endet beim ersten Korrespondenten, der keinen Schlüssel veröffentlicht, und die meisten tun das nicht. Eine Richtlinie ENCRYPT = required lässt einem Operator dann zwei schlechte Antworten: die Nachricht bouncen oder sie trotzdem im Klartext senden. Das Secure-Link-Portal ist die dritte: die Nachricht verschlüsselt auf dem Server halten und den Empfänger sie in einem Browser lesen lassen, nachdem er eine PIN eingegeben hat, die ihn auf einem anderen Weg erreicht hat.
Es ist der dritte Zweig der ON_NO_KEY-Richtlinie von pepsi-stage-encrypt; die anderen beiden und der Schlüsselspeicher, der entscheidet, welcher Zweig genommen wird, stehen in Schlüsselverwaltung.
12.1. Der Ablauf¶
Ein lokaler Benutzer sendet eine vertrauliche Nachricht.
pepsi-stage-encryptfindet keinen brauchbaren Schlüssel für den Empfänger und routet sie, weilON_NO_KEY = secure-linkgilt, zupepsi-stage-secure-link, statt sie weiterzuleiten.Diese Stage erzeugt eine PIN, leitet ein 192-Bit-URL-Token aus dem Warteschlangendatensatz ab (siehe pepsi-stage-secure-link(1)) und einen Inhaltsschlüssel aus der PIN, einem Salz je Nachricht und dem Server-Pfeffer (das Token ist keine Eingabe der Schlüsselableitung — es sind die zugehörigen Daten des AEAD), speichert nur das Chiffrat und löscht den Warteschlangendatensatz.
Der Empfänger erhält eine Benachrichtigungsmail mit einem Link. Der Absender erhält die PIN (der Standardwert
PIN_DELIVERY = sender) und gibt sie per Telefon, Kurznachricht oder persönlich weiter.Der Empfänger öffnet den Link, gibt die PIN ein und liest die Nachricht. Anhänge können heruntergeladen werden; ist
REPLY_STAGEkonfiguriert, kann eine Antwort geschrieben und an den Absender zurückgeschickt werden.Nach
EXPIRY_DAYSist die Nachricht nicht mehr lesbar, undpepsi-secure-link prunelöscht sie.
Die Pipeline sieht so aus:
[stage-encrypt]
PROGRAM = pepsi-stage-encrypt
ENCRYPT = required
SECURE_LINK_STAGE = secure-link
NEXT_STAGE = srs
[stage-secure-link]
PROGRAM = pepsi-stage-secure-link
[pepsi-secure-link]
BASE_URL = https://secure.example.org
NOTIFY_STAGE = srs
ORGANIZATION = Example Ltd
# PEPPER is written to secrets.d/pepsi-secure-link.secret by pepsi-setup.
NOTIFY_STAGE liegt auf dem ausgehenden Pfad, denn die Benachrichtigungs- und PIN-Mails sind Nachrichten, die die Installation selbst erzeugt und normal signieren und weiterleiten muss.
12.2. Was dies schützt und was nicht¶
12.2.1. Die gespeicherte Nachricht¶
Der Inhaltsschlüssel wird mit Argon2id (RFC 9106) aus drei Eingaben abgeleitet, die an drei verschiedenen Orten liegen. Argon2id statt eines einfachen Hashes oder PBKDF2, weil das gestreckte Geheimnis eine kurze PIN ist (standardmäßig zehn Zeichen aus einem Alphabet von 31 Symbolen, das verwechselbare Zeichen auslässt): Teuer gemacht werden muss eine parallele Offline-Suche, und nur eine speicherharte Funktion erhöht die Kosten der Hardware, die ein Angreifer verwenden würde, statt bloß der Zeit.
Die drei Eingaben:
die PIN, die den Empfänger außerhalb des Bandes erreicht und nirgends gespeichert wird;
ein Salz je Nachricht, das eine Datenbankspalte ist (ein Salz ist kein Geheimnis — es ist das, was verhindert, dass eine Vorberechnung jede Nachricht abdeckt);
ein serverseitiger Pfeffer, der nur in
secrets.d/pepsi-secure-link.secretliegt.
Gespeichert wird nur das AES-256-GCM-Chiffrat — ein AEAD, sodass die gespeicherte Nachricht ebenso authentifiziert wie vertraulich ist und ein manipuliertes Chiffrat sich nicht öffnen lässt, statt zu etwas zu entschlüsseln, das ein Angreifer gewählt hat. Folglich:
Eine gestohlene Datenbank ist kein Postfach. Ein Dump, ein Replikat oder ein Sicherungsband liefert Chiffrat und ein Salz, und weder die PIN noch der Pfeffer stecken darin.
Auch ein nachträglich kompromittierter Server ist kein Postfach. Ein Angreifer, der auch den Pfeffer erlangt, muss immer noch die PIN finden, und eine 10-Zeichen-PIN unter Argon2id ist nichts, was man durchsucht — für die Nachrichten, die gespeichert wurden, bevor er kam. Wer die Kontrolle über die Pipeline behält, sieht jede neue Nachricht im Klartext, wenn sie eingereicht wird, wie jede andere Mail in der Warteschlange.
Es gibt keinen Passwort-Verifizierer. Eine PIN zu prüfen und damit zu entschlüsseln sind dieselbe Operation — der AEAD-Tag ist die Prüfung —, sodass nichts Gespeichertes offline zu einer PIN geknackt werden kann.
Der Administrator kann eine gespeicherte Nachricht nicht lesen. Nicht mit der Datenbank, nicht mit dem Pfeffer, nicht mit beidem. Es gibt kein
pepsi-secure-link read, kein--decryptund keinen Hinterlegungsschlüssel. Dies ist eine Eigenschaft der gespeicherten Nachricht: Dem Operator wird Mail während der Übertragung anvertraut (Bedrohungsmodell), und ein Operator, der den Server verändert, kann eine Nachricht abfangen, bevor sie versiegelt wird.
Der Preis dieser letzten Eigenschaft: Eine verlorene PIN ist eine verlorene Nachricht. Der Wiederherstellungsweg ist, dass der Absender sie erneut sendet, was eine neue PIN und einen neuen Link ausstellt. Rechnen Sie damit, nach einem Hinterlegungsschlüssel gefragt zu werden; einen hinzuzufügen gäbe genau die Fähigkeit zurück, zu deren Beseitigung dieser Entwurf existiert.
Den Pfeffer zu verlieren hat dieselbe Form in größerem Maßstab: Jede gespeicherte Nachricht wird auf einen Schlag unlesbar. Sichern Sie ihn mit dem übrigen secrets.d, und behandeln Sie seine Rotation so, als entwerte sie jeden offenen Link.
12.2.2. Was nicht geschützt ist: die Metadaten¶
Bewusst im Klartext, als gewöhnliche Datenbankspalten:
der Absender und der Empfänger — die Benachrichtigung und der zweite Faktor müssen zugestellt werden, und „ist das angekommen?“ muss beantwortbar sein;
Zeitstempel — erstellt, läuft ab, erstmals gelesen;
Lebenszykluszähler — Lesezähler, fehlgeschlagene PIN-Versuche, Sperren, Antworten;
das Zugriffsprotokoll — wann jeder Versuch stattfand, von welcher Adresse und wie er endete.
Der Betreff steht nicht auf dieser Liste. Er ist meist das aufschlussreichste einzelne Feld — oft ist er die Nachricht („Biopsieergebnisse“, „Kündigungsschreiben“) —, und nichts an Routing oder Ablauf braucht ihn, sodass er im Chiffrat neben dem Nachrichtentext liegt.
Eine Operator-Konsole listet daher wer und wann, nie was, und die Lesebestätigung ebenso. Der Support wird entsprechend schwerer: „die Nachricht, die Sie um 09:14 an Bob geschickt haben, wurde um 11:02 gelesen“ ist das Meiste, was hier jemand sagen kann, und es gibt keine Möglichkeit zu bestätigen, was darin stand.
Das Zugriffsprotokoll hält die Peer-Adresse fest und sonst nichts — keinen User-Agent und keine Geolokalisierung.
12.2.3. Der zweite Faktor¶
Die ganze Konstruktion beruht darauf, dass die PIN über einen Kanal reist, den das Postfach des Empfängers nicht kontrolliert. PIN_DELIVERY wählt ihn:
sender(Standardwert)Die PIN wird an den Absender gemailt, der sie weitergibt. Keine externe Abhängigkeit, es funktioniert überall, und der Kanal ist einer, dem die beiden Korrespondenten bereits vertrauen. Es hängt allerdings davon ab, dass der Absender tatsächlich einen zweiten Kanal nutzt — die PIN an dieselbe Adresse weiterzumailen würde es zunichtemachen, und die PIN-Mail sagt das.
commandDie PIN wird einem konfigurierten Programm (einem SMS-Gateway, einem betrieblichen Messaging-Hook) auf der Standardeingabe übergeben. Die stärkste Option, wo ein echter zweiter Kanal existiert.
noneDeutlich schwächer, und es sollte bewusst gewählt werden. Der Link selbst trägt die PIN, sodass jeder, der das Postfach des Empfängers lesen kann, die Nachricht lesen kann — was genau das ist, was die Funktion verhindern soll. Es setzt die PIN außerdem in eine URL und damit potenziell in einen Browserverlauf oder in die Journale eines Zwischenträgers. Verwenden Sie es nur dort, wo das Postfach wirklich der Faktor ist.
12.2.4. Brute Force¶
Eine PIN, die kurz genug zum Vorlesen ist, ist kurz genug zum Raten, daher wird das Raten zweifach begrenzt. Je Token sperren MAX_ATTEMPTS falsche PINs (standardmäßig 10) es für LOCKOUT (15 Minuten), und das Fenster verdoppelt sich mit jeder weiteren Sperre, bis zum 64-Fachen der Basis; während einer Sperre wird selbst die richtige PIN abgelehnt. Der Fehlerzähler wird zurückgesetzt, wenn eine Sperre beginnt, sodass das nächste Fenster ein frisches Budget ist statt einer sofortigen erneuten Sperre. Je Quelle begrenzt RATE_LIMIT die Anfragen pro Minute über alle Portalrouten hinweg, was auch verhindert, dass die bewusst teure Schlüsselableitung zu einem Weg wird, den Speicher des Servers zu verbrauchen.
Nur ein tatsächlicher Rateversuch wird gezählt. Ein Formular, das mit leerem PIN-Formularfeld abgeschickt wird — oder mit nichts, was die Normalisierung behält —, wird abgelehnt, ohne den Zähler zu berühren und ohne die Ableitung auszuführen. Es sagt einem Angreifer nichts, was er nicht schon hatte, und es zu zählen ließe jeden Client, der das Formular leer erneut absendet (ein doppeltes Absenden, ein Prefetch, ein Weiterleitungsverfolger, der die Methode beibehält), einen legitimen Empfänger aus seiner eigenen Nachricht aussperren.
Eine bestehende Sitzung unterliegt der Sperre nicht, sodass ein Dritter, der am selben Token rät, einen legitimen Leser nicht mitten im Lesen aussperren kann.
12.2.5. Der Browser¶
Angreiferkontrollierte Mail in einem Browser zu rendern ist das größte Risiko, das diese Funktion trägt, und ihm wird begegnet, indem es nicht getan wird:
Die Nachricht wird als Text gerendert, nie als HTML. Eine reine HTML-Nachricht wird zu Text gefaltet; Markup überlebt nicht. Es gibt daher keinen HTML-Bereiniger zu umgehen und keine entfernten Inhalte — die Richtlinie sagt
img-src 'none'(img-src data:, wenn eineLOGO_FILEeingebettet wird) und meint es auch so, sodass ein Tracking-Pixel nicht melden kann, dass die Nachricht geöffnet wurde. Formatierung und eingebettete Bilder gehen verloren; ein Empfänger, der das Original braucht, kann die Teile herunterladen.Jeder dynamische Wert auf einer Seite — Betreff, Absender, Datum, Dateiname, Organisationsname — wird an der Stelle HTML-escaped, an der er in die Seite geschrieben wird.
Jede Antwort trägt
Content-Security-Policy: default-src 'none'mit einer Nonce je Antwort für das eine eingebettete Stylesheet und überhaupt keiner benannten Skriptquelle, dazuReferrer-Policy: no-referrer,X-Content-Type-Options: nosniff,X-Frame-Options: DENYundCache-Control: no-store.Ein Anhang wird stets als
application/octet-streamalsattachmentmit einer Sandbox-Richtlinie ausgeliefert, welchen Typ die Nachricht auch behauptet hat.
12.3. Antworten¶
In einem reinen Leseportal kann der Empfänger vertraulich lesen, muss aber im Klartext antworten, was viel von dem zurückgibt, was die Funktion erkauft hat. Antworten wird daher unterstützt. Weil es ein Nachrichteneinspeisungs-Endpunkt im öffentlichen Web ist, sind die Beschränkungen darin eingebaut, statt dem Verfahren überlassen zu werden:
Die Antwort geht nur an den ursprünglichen Absender, und ihr Umschlagabsender wird auf den gespeicherten Empfänger festgelegt. Keines von beidem ist eine Eingabe, sodass es nichts zu missbrauchen gibt — das ist es, was das Portal davon abhält, ein offenes Relay zu sein.
Die injizierte Nachricht trägt kein
state.local_origin. Sie tritt beiREPLY_STAGEals das ein, was sie ist: eingehende Mail, die zufällig auf unserem eigenen Webserver entstanden ist. Sie kann daher nicht den Submission-Pfad nehmen — kein Weiterleiten als wir, kein Auto-Whitelisting, kein SRS als lokaler Ursprung.Sie wird nicht als eine Ihrer Domains DKIM-signiert. Der Autor ist ein Dritter;
pepsi-setupwarnt, wennREPLY_STAGEauf eine signierende Stage auflöst. Die Antwort trägt einenX-Pepsi-Secure-Link-Reply: …; author-unverified-Header, sodass der Empfänger erkennen kann, wie sie eintraf.Der Nachrichtentext ist text/plain, was auch immer getippt wurde. HTML zu verfassen hieße, auch ausgehendes HTML zu bereinigen und nicht nur eingehendes.
Anhänge sind erlaubt und gedeckelt — Gesamtgröße, Dateianzahl und Antworten je Nachricht —, wobei die Größenobergrenze während des laufenden Hochladens durchgesetzt wird, sodass ein Client, der
Content-Lengthfalsch angibt, nichts gewinnt. Uploads werden nie auf die Platte geschrieben: Sie gehen direkt in die injizierte Nachricht, sodass es keine zweite Speicherlebensdauer gibt und nichts zurückbleibt, wenn die Injektion fehlschlägt.Jede Antwort wird im Zugriffsprotokoll der Nachricht festgehalten.
Antworten ist aus, sofern REPLY_STAGE nicht gesetzt ist.
12.4. Wohin mit dem Portal¶
Ein eigener Hostname ist die Empfehlung — ein DNS-Eintrag und ein certbot-SAN erkaufen echte Origin-Trennung —, aber er ist nicht erforderlich, und ein gemeinsamer Origin ist konstruktionsbedingt sicher statt durch Disziplin:
Das Sitzungs-Cookie ist auf
Path=/secure/beschränkt, sodass es selbst auf einem Host nie an/metrics,/resumeoder das Web Key Directory gesendet wird;sein Wert ist ein unter dem Server-Pfeffer versiegeltes AEAD, sodass ein von einer Geschwister-Subdomain platziertes Cookie sich nicht öffnet — was sonst nach einem
__Host--Präfix verlangt hätte (einem Präfix, das RFC 6265bis mit Pfadbeschränkung unvereinbar macht, da esPath=/verlangt);es ist
HttpOnly,Secure,SameSite=Strictund host-only (keinDomain);die Sicherheits-Header werden je Route gesetzt, nicht je Host, sodass die enge Richtlinie des Portals gilt, wo auch immer es eingehängt ist.
Das Portal muss vom Browser aus über HTTPS erreichbar sein: Das Sitzungs-Cookie trägt Secure. Ein TLS-terminierender Reverse-Proxy davor ist in Ordnung.
12.5. Speicherung und Dimensionierung¶
Das Chiffrat liegt als BYTEA in der Datenbank. Das hält alles transaktional — eine Secure-Nachricht zu erzeugen ist ein Insert, das Aufräumen ein DELETE, und Sicherung und Wiederherstellung decken den Inhalt automatisch mit ab, ohne verwaiste Dateien und ohne ein zweites Ding, bei dem man die Rechte richtig setzen muss.
Der Preis ist Datenbankgröße und WAL-Umsatz proportional zum Portalverkehr. Eine Nachricht wird je schlüssellosem Empfänger einmal gespeichert (jeweils mit eigener PIN und eigenem Chiffrat, sodass die PIN eines Empfängers nie die Kopie eines anderen öffnet), und [stage-encrypt] MAX_SIZE (standardmäßig 25 MB) begrenzt jede einzelne. Dimensionieren Sie eine Installation entsprechend und lassen Sie pepsi-secure-link prune täglich laufen — das Portal weist ein abgelaufenes Token bereits ab, aber nichts löscht die Bytes, bevor die Aufgabe läuft.
12.6. Es betreiben¶
pepsi-secure-link list zeigt, was offen ist; show ergänzt das Zugriffsprotokoll einer Nachricht; revoke zerstört eine Nachricht (es legt die Mail nicht zurück in die Warteschlange — es zieht sie zurück); prune löscht, was abgelaufen ist. Siehe pepsi-secure-link(1).
Die Datenbankgrenze ist enger als im übrigen Schema und wird bei jedem pepsi-setup run verifiziert: Nur die Rolle pepsi-httpd kann secure_message.ciphertext lesen. Die Rolle pepsi — unter der jeder Stage-Worker läuft, auch die, die feindliche Mail parsen — kann eine Nachricht anlegen, die Metadaten auflisten und einen Datensatz löschen, und kann keine wieder auslesen. pepsi-ingress und die übrigen können die Tabellen überhaupt nicht berühren.
12.7. Erscheinungsbild¶
ORGANIZATION, BRAND_COLOR und LOGO_FILE bestimmen das Erscheinungsbild der Seiten; ORGANIZATION wird außerdem in den Benachrichtigungsmails genannt. Die Farbe muss eine hexadezimale Farbe sein und das Logo ein lokales Rasterbild, beides aus demselben Grund: Sie werden in eine Seite interpoliert, und ein Wert, der beliebiger Text oder ein SVG sein könnte, würde die Angriffsfläche der Seite vergrößern. Die drei Benachrichtigungsmails selbst sind Mustache-Vorlagen (secure-link, secure-pin, secure-receipt) unter [pepsi] TEMPLATE_DIR und können vollständig neu geschrieben werden.
12.8. Siehe auch¶
Schlüsselverwaltung (der Speicher und die Entdeckungsschicht, die entscheiden, ob ein Empfänger diesen Ausweg überhaupt braucht), pepsi-stage-encrypt (ON_NO_KEY = secure-link), pepsi-stage-secure-link, pepsi-secure-link (das Operator-CLI), pepsi-httpd (das das Portal bedient), pepsi.conf(5).