Fix #3190 (TOFU the SPKI hash and not the whole cert)

Makes letsencrypt renewals more pleasant.
Thanks jonasw and aioxmpp for the ASN.1 wizardry
This commit is contained in:
mathieui 2017-10-10 00:52:44 +02:00
parent dcdc970acd
commit ef84a109e8
3 changed files with 28 additions and 18 deletions

View file

@ -62,11 +62,16 @@ and certificate validation.
**Default value:** ``[empty]``
The SHA-2 fingerprint of the SSL certificate as a hexadecimal string,
you should not touch it, except if know what you are doing.
The SHA-2 fingerprint of the SubjectPublicKeyInfo of the SSL
certificate as a hexadecimal string, you should not touch it,
except if know what you are doing.
.. note:: the fingerprint was previously stored in SHA-1, and has been
silently upgraded to SHA-2 if the SHA-1 still matched.
.. note:: the fingerprint was previously a fingerprint of the whole
certificate, while it is now only of the SubjectPublicKeyInfo,
which persists across LetsEncrypt renewals, and therefore
reduces the noise generated by the alert dialog.
.. versionchanged:: 0.12
ciphers

View file

@ -13,9 +13,12 @@ import ssl
import sys
import time
from datetime import datetime
from hashlib import sha1, sha512
from hashlib import sha256, sha512
from os import path, makedirs
import pyasn1.codec.der.decoder
import pyasn1.codec.der.encoder
import pyasn1_modules.rfc2459
from slixmpp import InvalidJID
from slixmpp.xmlstream.stanzabase import StanzaBase, ElementBase
from xml.etree import ElementTree as ET
@ -54,9 +57,9 @@ This can be part of a normal renewal process, but can also mean that \
an attacker is performing a man-in-the-middle attack on your connection.
When in doubt, check with your administrator using another channel.
SHA-512 of the old certificate: %s
SHA-256 of the old certificate (SPKI): %s
SHA-512 of the new certificate: %s
SHA-256 of the new certificate (SPKI): %s
"""
HTTP_VERIF_TEXT = """
@ -1357,24 +1360,26 @@ class HandlerCore:
config.set_and_save('certificate', cert)
der = ssl.PEM_cert_to_DER_cert(pem)
sha1_digest = sha1(der).hexdigest().upper()
sha1_found_cert = ':'.join(i + j for i, j in zip(sha1_digest[::2], sha1_digest[1::2]))
asn1 = pyasn1.codec.der.decoder.decode(der, asn1Spec=pyasn1_modules.rfc2459.Certificate())[0]
spki = asn1.getComponentByName("tbsCertificate").getComponentByName("subjectPublicKeyInfo")
spki_digest = sha256(pyasn1.codec.der.encoder.encode(spki)).hexdigest().upper()
spki_found_cert = ':'.join(i + j for i, j in zip(spki_digest[::2], spki_digest[1::2]))
sha2_digest = sha512(der).hexdigest().upper()
sha2_found_cert = ':'.join(i + j for i, j in zip(sha2_digest[::2], sha2_digest[1::2]))
if cert:
if sha1_found_cert == cert:
log.debug('Current hash is SHA-1, moving to SHA-2 (%s)',
sha2_found_cert)
config.set_and_save('certificate', sha2_found_cert)
if sha2_found_cert == cert:
log.debug('Current hash is cert hash, moving to SPKI hash (%s)',
spki_found_cert)
config.set_and_save('certificate', spki_found_cert)
return
elif sha2_found_cert == cert:
elif spki_found_cert == cert:
return
else:
self._ssl_pop_tab(cert, sha2_found_cert)
self._ssl_pop_tab(cert, spki_found_cert)
else:
log.debug('First time. Setting certificate to %s', sha2_found_cert)
if not config.silent_set('certificate', sha2_found_cert):
log.debug('First time. Setting certificate to %s', spki_found_cert)
if not config.silent_set('certificate', spki_found_cert):
self.core.information('Unable to write in the config file', 'Error')
def http_confirm(self, stanza):

View file

@ -106,7 +106,7 @@ setup(name="poezio",
('share/poezio/', ['README.rst', 'COPYING', 'CHANGELOG'])]
+ find_doc('share/doc/poezio/source', 'source')
+ find_doc('share/doc/poezio/html', 'build/html')),
install_requires=['slixmpp>=1.2.4', 'aiodns'],
install_requires=['slixmpp>=1.2.4', 'aiodns', 'pyasn1', 'pyasn1_modules'],
extras_require={'OTR plugin': 'python-potr>=1.0',
'Screen autoaway plugin': 'pyinotify==0.9.4'})