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]`` **Default value:** ``[empty]``
The SHA-2 fingerprint of the SSL certificate as a hexadecimal string, The SHA-2 fingerprint of the SubjectPublicKeyInfo of the SSL
you should not touch it, except if know what you are doing. 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 .. note:: the fingerprint was previously a fingerprint of the whole
silently upgraded to SHA-2 if the SHA-1 still matched. 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 ciphers

View file

@ -13,9 +13,12 @@ import ssl
import sys import sys
import time import time
from datetime import datetime from datetime import datetime
from hashlib import sha1, sha512 from hashlib import sha256, sha512
from os import path, makedirs 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 import InvalidJID
from slixmpp.xmlstream.stanzabase import StanzaBase, ElementBase from slixmpp.xmlstream.stanzabase import StanzaBase, ElementBase
from xml.etree import ElementTree as ET 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. an attacker is performing a man-in-the-middle attack on your connection.
When in doubt, check with your administrator using another channel. 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 = """ HTTP_VERIF_TEXT = """
@ -1357,24 +1360,26 @@ class HandlerCore:
config.set_and_save('certificate', cert) config.set_and_save('certificate', cert)
der = ssl.PEM_cert_to_DER_cert(pem) der = ssl.PEM_cert_to_DER_cert(pem)
sha1_digest = sha1(der).hexdigest().upper() asn1 = pyasn1.codec.der.decoder.decode(der, asn1Spec=pyasn1_modules.rfc2459.Certificate())[0]
sha1_found_cert = ':'.join(i + j for i, j in zip(sha1_digest[::2], sha1_digest[1::2])) 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_digest = sha512(der).hexdigest().upper()
sha2_found_cert = ':'.join(i + j for i, j in zip(sha2_digest[::2], sha2_digest[1::2])) sha2_found_cert = ':'.join(i + j for i, j in zip(sha2_digest[::2], sha2_digest[1::2]))
if cert: if cert:
if sha1_found_cert == cert: if sha2_found_cert == cert:
log.debug('Current hash is SHA-1, moving to SHA-2 (%s)', log.debug('Current hash is cert hash, moving to SPKI hash (%s)',
sha2_found_cert) spki_found_cert)
config.set_and_save('certificate', sha2_found_cert) config.set_and_save('certificate', spki_found_cert)
return return
elif sha2_found_cert == cert: elif spki_found_cert == cert:
return return
else: else:
self._ssl_pop_tab(cert, sha2_found_cert) self._ssl_pop_tab(cert, spki_found_cert)
else: else:
log.debug('First time. Setting certificate to %s', sha2_found_cert) log.debug('First time. Setting certificate to %s', spki_found_cert)
if not config.silent_set('certificate', sha2_found_cert): if not config.silent_set('certificate', spki_found_cert):
self.core.information('Unable to write in the config file', 'Error') self.core.information('Unable to write in the config file', 'Error')
def http_confirm(self, stanza): def http_confirm(self, stanza):

View file

@ -106,7 +106,7 @@ setup(name="poezio",
('share/poezio/', ['README.rst', 'COPYING', 'CHANGELOG'])] ('share/poezio/', ['README.rst', 'COPYING', 'CHANGELOG'])]
+ find_doc('share/doc/poezio/source', 'source') + find_doc('share/doc/poezio/source', 'source')
+ find_doc('share/doc/poezio/html', 'build/html')), + 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', extras_require={'OTR plugin': 'python-potr>=1.0',
'Screen autoaway plugin': 'pyinotify==0.9.4'}) 'Screen autoaway plugin': 'pyinotify==0.9.4'})