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:
parent
dcdc970acd
commit
ef84a109e8
3 changed files with 28 additions and 18 deletions
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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):
|
||||||
|
|
2
setup.py
2
setup.py
|
@ -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'})
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue