#3903: Rework OpenSSL certificate verification to support alternative chains
-------------------------+----------------------
 Reporter:  kempniu      |      Owner:  mutt-dev
     Type:  enhancement  |     Status:  new
 Priority:  minor        |  Milestone:
Component:  crypto       |    Version:
 Keywords:               |
-------------------------+----------------------
 The way Mutt currently verifies SSL certificates using OpenSSL does not
 support alternative chains, which may cause confusion when some popular
 mail providers (e.g. Gmail) are used with specific sets of trusted CA
 certificates.

 I am using Mutt 1.7.2 on Arch Linux, which currently ships with CA
 certificates distributed as part of Mozilla's NSS 3.27.1.  One
 particularly interesting thing about that release is that
 [https://bugzilla.mozilla.org/show_bug.cgi?id=1296689 some CAs were
 removed in it], specifically the Equifax Secure Certificate Authority.  As
 of today, Gmail IMAP servers are using a certificate chain which ends with
 a certificate (attached) that was issued to !GeoTrust by Equifax.  When
 trying to use Gmail with the trusted CA store from NSS 3.27.1, things get
 awkward, because the signer of the !GeoTrust certificate is not present in
 the trusted CA store.  However, the latter contains another certificate
 (also attached) issued to !GeoTrust that is self-signed (both the removed
 Equifax-signed certificate and the self-signed one have the same subject).
 This is where alternative chains step in.  Default verification routines
 implemented in newer OpenSSL versions (1.0.2b+) have them enabled by
 default, which in this case causes the Equifax-signed !GeoTrust
 certificate to be replaced with its self-signed version during
 verification.  This in turn prevents any untrusted certificate warnings
 from appearing.

 However, for an alternative chain to be found, the `X509_STORE_CTX`
 structure supplied to `X509_verify_cert()` must have a non-`NULL`
 `untrusted` member.  Meanwhile, Mutt tries to build a trust chain on its
 own, starting from the root and adding certificates to `SslSessionCerts`
 as it goes, which means that when `X509_verify_cert()` is first called
 from `check_certificate_by_signer()`, `SslSessionCerts` will be `NULL`,
 causing the `untrusted` member of `X509_STORE_CTX` to be `NULL` and thus
 making it impossible for OpenSSL to look for an alternative chain.  This
 in turn causes the certificate to be displayed to the user as untrusted.

 In the case of Gmail, things get even more confusing if the user chooses
 to reject the Equifax-signed !GeoTrust certificate, because the `for` loop
 moves on to the next certificate in the chain, i.e. the one issued to
 Google by !GeoTrust and this time verification succeeds, because the
 signer of this certificate is in the trust store (due to the self-signed
 !GeoTrust certificate being present).  The end result is that the user
 chooses to reject a certificate and the connection succeeds anyway.

 The confusing prompt can obviously be prevented from appearing by adding
 the Equifax-signed !GeoTrust certificate (or the Equifax self-signed
 certificate removed in NSS 3.27.1) to `~/.mutt_certificates`, but I
 decided to attempt a proper fix by reworking the way Mutt verifies
 certificates using OpenSSL, implementing a solution which I believe is a
 bit closer to how OpenSSL is typically used.  I removed
 `ssl_check_certificate()` altogether, instead enabling peer certificate
 verification (which causes `X509_verify_cert()` to be called automatically
 when `SSL_connect()` is called) and setting the verify callback to a
 modified version of `ssl_check_preauth()`.  It seemed reasonable as
 `ssl_check_certificate()` duplicates much of the work that
 `X509_verify_cert()` does automatically when called with a complete
 certificate chain supplied by the peer (it walks the chain starting from
 the root and tries to validate each certificate along the path based on
 what it has learned so far).  Using this approach together with some
 `SSL_CTX_*` calls enabled me to also get rid of
 `check_certificate_by_signer()`, making the code a bit shorter and
 hopefully cleaner.  The patch also causes the connection to be immediately
 interrupted at the SSL handshake level if the user rejects any certificate
 in the chain, which I feel is the intuitive thing to do.

 This patch likely needs further work and definitely needs a thorough
 review and testing.  Hopefully the code itself is easier to understand
 than the convoluted explanation above.  If you think this is a move in the
 right direction, I will be happy to further polish the patch to your
 liking.  One issue I can already think of is that the user might want to
 have the possibility to turn alternative chains off through the
 configuration file to retain old behavior.  I can implement that as well,
 but I think it deserves a separate patch for clarity.

--
Ticket URL: <https://dev.mutt.org/trac/ticket/3903>
Mutt <http://www.mutt.org/>
The Mutt mail user agent

Reply via email to