OCSP ist schwer

Ich betreibe ja schon etwas länger eine eigne CA. Seit 1998 wechsle ich jährlich die Root-CA Schlüssel, die jeweils zwei Jahre gültig und so überlappend wirksam sind. Und natürlich habe ich dafür einen OCSP Responder selbst schreiben müssen, weil es die Idee erst seit 1999 gibt. Die aktuellen Vorfälle von LetsEncrypt demonstrieren, wie wichtig es ist, sich intensiv mit den Clients zu befassen.

Aufgabestellung

OSCP ist ein Protokoll, bei dem online bei der CA nachgefragt werden kann, ob ein bestimmtes Zertifikat noch gültig ist.

Vor OCSP hatten wir CRLs, also Listen von zurückgerufenen Zertifikaten.

Ich ziehe OSCP der CRL aus mehreren Gründen vor:

  • Eine CRL muss regelmäßig neu erstellt und übertragen werden.
  • Eine CRL muss sämtliche Rückrufe enthalten, während der Gültigkeit der CA-Schlüssel ausgestellt wurden.
  • Damit Clients mit einer (i.d.R. riesigen) CRL etwas anfangen können, müssen sie diese einige Stunden oder Tage cachen, bevor sie eine neue Liste übertragen. In der Zeit bekommen Sie von Zertifikatsrückrufen nichts mit.
  • OCSP ist live, d.h. die Reaktion auf einen Zertifikatsrückruf ist praktisch unmittelbar.
  • OCSP beantwortet nur die konkrete Frage, ob ein bestimmtes Zertifikat einer bestimmten CA jetzt gültig ist.
  • Die (kleine) OCSP-Antwort hat ebenfalls einen (kleineren) Timeout und kann solange gecached werden.
  • OCSP lässt sich seitens des Websservers zentralisieren und mit an die Clients ausliefern. Die Clients müssen dann nicht selbst bei der CA anfragen.

Das Protokoll sieht vor, dass der OCSP Dienst in der Anfrage gesagt bekommt, um welche Seriennummer des Zertifikat es geht und von welcher CA die ausgestellt wurde. Das ist wichtig, wenn man – wie ich – zwei CAs parallel betreibt.

Die Anfrage ist ein ASN.1-Binärblob und kann auf zwei verschiedene Arten gestellt werden:

  • Als POST-Request mit einem binären Blob als Payload.
  • Als GET-Request mit der BASE64-codierten Variante des Blobs.

So weit, so einfach.

Implementierung

Als ich damit anfing, war die einzige verfügbare Software openssl.

Anfangs habe ich diesen als Dienst einfach laufen lassen, aber das ging nicht lange gut.

  • Zum einen kann der Dienst nur mit einer CA arbeiten, ich musste also immer die Neuste nehmen.
  • Anfragen zu Bestandszertifikaten der alten CA wurden mit einem Kenn-ich-nicht-Fehler abgewiesen.
  • Die Software ist im Serverbetrieb instabil, sie dient eigentlich nur dem schnellen Test.

Mit der zunehmenden Validierung von Zertifikatsketten im Browser, wurden OCSP-Fehler zu einem ernsthaften Problem. Also musste es anders gelöst werden.

Als Server dient nun der normale Apache, der ein PHP-Script aufruft, das dann die OSCP Validierung durchführen kann.

<VirtualHost *:8888>
  ServerName ocsp.iks-jena.de
  AcceptPathInfo On
  AllowEncodedSlashes On
  RewriteEngine On
  RewriteRule .* /index.php [L]
</VirtualHost>

In den Zertifikaten wird als OCSP-URL //ocsp.iks-jena.de:8888/ angegeben. Das ist konsistent mit dem Betriebsmodus des openssl Servers.

Anfragen an die URL werden dem PHP-Script vorgeworfen, was für POST-Requests anstandslos klappt.

Problematisch sind die GET-Requests. Die schauen so aus:

"GET //MEMwQTA%2FMD0wOzAJBgUrDgMCGgUABBRFLlbvANe4EFL%2F%2Fg56rSJuL2puEQQULWVsPIwYJRoGyp0azlVnHz5%2FsOoCAgIf HTTP/1.1" 200 2081 "-" "Microsoft-CryptoAPI/6.3"
"GET /MGgwZjA/MD0wOzAJBgUrDgMCGgUABBQmq5X/jwfX2qs7ZfkkUfZHOvrMbAQU6DQvbfzWJ21xpLYn9t9RpDf4gY4CAgICoiMwITAfBgkrBgEFBQcwAQIEEgQQw9lx90TbtLT+H7mqNucYhg== HTTP/1.1" 200 2125 "-" "Mozilla/5.0 (X11; FreeBSD amd64; rv:53.0) Gecko/20100101 Firefox/53.0"

Es gibt aber auch beliebige Mischformen, bei denen der initale Slash doppelt oder einfach ist, die Slashes, Gleichheits- oder Pluszeichen im String literal da stehen oder urlencoded sind.

Es ist dabei nicht einmal sicher gestellt, das eine bestimmte Codierung konsequent eingehalten wird. Proxies neigen bspw. dazu, selbst einige Zeichen zu encodieren. Im Extemfall es sogar auftreten, dass auch normale Buchstaben encodiert werden.

Was tut nun der Apache?

  • Mit AcceptPathInfo regt er sich nicht darüber auf, dass die URL einen nicht existenten Pfad referenziert, denn eine Datei dieses Namens existiert ganz offensichtlich nicht.
  • Mit AllowEncodedSlashes weist der Apache encodierte Slashes nicht mehr ab, die er sonst für einen Angriff hält und die Bearbeitung verweigert.

Im PHP Script kann ich dann schauen, welche CA angefragt wurde und die passende OCSP Antwort für die jeweilige CA bereit stellen.

Das tut einfach schmerzfrei.

Slash me

Nun zur Frage, wie man mit führenden Slashes umgeht. Ab wann ist der Slash Bestandteil der Anfrage und bis wann ist er noch Ergebnis der Anfragekonstruktion der Software.

Die Software bekommt aus dem Zertifikat eine OCSP-URL und muss dort ihren Request anhängen. Einige Software benutzt dazu "%s/%s" und andere benutzt "%s%s". Für beide Varianten gibt es gute Gründe.

In meinem Fall hat die OCSP-URL aber keinen Pfad. Der URI-Standard gibt es aber nicht her, den abschließenden Slash wegzulassen, um das Ende des Hostnamens zu markieren. Also hat meine URL einen finalen syntaktischen Slash, der allerdings semantisch gar nicht existiert.

Eine anders konstruierte OCSP-URL könnte lauten http://ocsp.example:12345/ocsp.asp?req=. Hier soll offensichtlich kein Slash angefügt werden.

Noch anders wäre der Fall bei http://ocsp.example:54321/cgi-bin/ocsp.pl. Hier wird erwartet, dass der Slash literal (und nicht encoded) eingefügt wird, weil sonst das Script nicht gefunden werden kann.

Das eigentliche Problem liegt also darin begründet, dass der OCSP Standard leichtsinnig eine Codierung gewählt hat, die reservierte Zeichen im benutzten Protokoll generieren kann. Kurz gesagt: Die Norm ist fehlerhaft.

Aber sie ist draußen und wir müssen damit leben.

Die entscheidende Frage ist also, wie man entscheiden kann, wo die realen Daten anfangen. Dazu muss ins Protokoll geschaut werden.

Es liegt immer eine ASN.1 Seqzenz vor:

OCSPRequest     ::=     SEQUENCE {
 tbsRequest                  TBSRequest,
 optionalSignature   [0]     EXPLICIT Signature OPTIONAL
} 
TBSRequest      ::=     SEQUENCE {
 version             [0]     EXPLICIT Version DEFAULT v1,
 requestorName       [1]     EXPLICIT GeneralName OPTIONAL,
 requestList                 SEQUENCE OF Request,
 requestExtensions   [2]     EXPLICIT Extensions OPTIONAL
}

Das erste ASN.1 Byte für diesen Anfang ist also 0x30 (SEQUENCE). Die Dekodierung eines Slashes aus Base64 heraus ist mindestens 0xfc.

Damit ist es unmöglich, dass ein Slash den Anfang eines base64 kodierten OSCP-Requests darstellen kann. Im Gegenteil, das erste Zeichen einer ASN.1 Sequenz in Base64 ist zwingend ein M (genau genommen kann der Base64 Teil nur mit MA bis MP anfangen.

Die praktische Lösung des Problems ist also einfach:

  • Man dekodiert alles, was nach URLEncode aussieht.
  • Man entfernt alle führenden Slashes.
  • Man dekodiert den Rest mit Base64.

Fazit

Alles, was man tun muss, ist ins Logfile schauen. Das Internet existiert nur wegen eines Grundsatzes: Be conservative in what you do, be liberal in what you accept from others.

Post a comment

Related content