Merge pull request #372 from YunoHost/tsig-sha256

Uses hmac-sha512 for dyndns TSIG
This commit is contained in:
Laurent Peuch 2018-01-24 10:03:34 +01:00 committed by GitHub
commit be8ae067e6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 172 additions and 31 deletions

View file

@ -213,6 +213,14 @@
"mailbox_used_space_dovecot_down": "Dovecot mailbox service need to be up, if you want to get mailbox used space",
"maindomain_change_failed": "Unable to change the main domain",
"maindomain_changed": "The main domain has been changed",
"migrate_tsig_end": "Migration to hmac-sha512 finished",
"migrate_tsig_failed": "Migrating the dyndns domain {domain} to hmac-sha512 failed, rolling back. Error: {error_code} - {error}",
"migrate_tsig_start": "Not secure enough key algorithm detected for TSIG signature of domain '{domain}', initiating migration to the more secure one hmac-sha512",
"migrate_tsig_wait": "Let's wait 3min for the dyndns server to take the new key into account...",
"migrate_tsig_wait_2": "2min...",
"migrate_tsig_wait_3": "1min...",
"migrate_tsig_wait_4": "30 secondes...",
"migrate_tsig_not_needed": "You do not appear to use a dyndns domain, so no migration is needed !",
"migrations_backward": "Migrating backward.",
"migrations_bad_value_for_target": "Invalide number for target argument, available migrations numbers are 0 or {}",
"migrations_cant_reach_migration_file": "Can't access migrations files at path %s",

View file

@ -0,0 +1,91 @@
import glob
import os
import requests
import base64
import time
import json
import errno
from moulinette import m18n
from moulinette.core import MoulinetteError
from moulinette.utils.log import getActionLogger
from yunohost.tools import Migration
from yunohost.dyndns import _guess_current_dyndns_domain
logger = getActionLogger('yunohost.migration')
class MyMigration(Migration):
"Migrate Dyndns stuff from MD5 TSIG to SHA512 TSIG"
def backward(self):
# Not possible because that's a non-reversible operation ?
pass
def migrate(self, dyn_host="dyndns.yunohost.org", domain=None, private_key_path=None):
if domain is None or private_key_path is None:
try:
(domain, private_key_path) = _guess_current_dyndns_domain(dyn_host)
assert "+157" in private_key_path
except (MoulinetteError, AssertionError):
logger.warning(m18n.n("migrate_tsig_not_needed"))
return
logger.warning(m18n.n('migrate_tsig_start', domain=domain))
public_key_path = private_key_path.rsplit(".private", 1)[0] + ".key"
public_key_md5 = open(public_key_path).read().strip().split(' ')[-1]
os.system('cd /etc/yunohost/dyndns && '
'dnssec-keygen -a hmac-sha512 -b 512 -r /dev/urandom -n USER %s' % domain)
os.system('chmod 600 /etc/yunohost/dyndns/*.key /etc/yunohost/dyndns/*.private')
# +165 means that this file store a hmac-sha512 key
new_key_path = glob.glob('/etc/yunohost/dyndns/*+165*.key')[0]
public_key_sha512 = open(new_key_path).read().strip().split(' ', 6)[-1]
try:
r = requests.put('https://%s/migrate_key_to_sha512/' % (dyn_host),
data={
'public_key_md5': base64.b64encode(public_key_md5),
'public_key_sha512': base64.b64encode(public_key_sha512),
}, timeout=30)
except requests.ConnectionError:
raise MoulinetteError(errno.ENETUNREACH, m18n.n('no_internet_connection'))
if r.status_code != 201:
try:
error = json.loads(r.text)['error']
except Exception:
# failed to decode json
error = r.text
import traceback
from StringIO import StringIO
stack = StringIO()
traceback.print_stack(file=stack)
logger.error(stack.getvalue())
# Migration didn't succeed, so we rollback and raise an exception
os.system("mv /etc/yunohost/dyndns/*+165* /tmp")
raise MoulinetteError(m18n.n('migrate_tsig_failed', domain=domain,
error_code=str(r.status_code), error=error))
# remove old certificates
os.system("mv /etc/yunohost/dyndns/*+157* /tmp")
# sleep to wait for dyndns cache invalidation
logger.warning(m18n.n('migrate_tsig_wait'))
time.sleep(60)
logger.warning(m18n.n('migrate_tsig_wait_2'))
time.sleep(60)
logger.warning(m18n.n('migrate_tsig_wait_3'))
time.sleep(30)
logger.warning(m18n.n('migrate_tsig_wait_4'))
time.sleep(30)
logger.warning(m18n.n('migrate_tsig_end'))
return

View file

@ -27,6 +27,7 @@ import os
import re
import json
import glob
import time
import base64
import errno
import requests
@ -46,6 +47,14 @@ OLD_IPV4_FILE = '/etc/yunohost/dyndns/old_ip'
OLD_IPV6_FILE = '/etc/yunohost/dyndns/old_ipv6'
DYNDNS_ZONE = '/etc/yunohost/dyndns/zone'
RE_DYNDNS_PRIVATE_KEY_MD5 = re.compile(
r'.*/K(?P<domain>[^\s\+]+)\.\+157.+\.private$'
)
RE_DYNDNS_PRIVATE_KEY_SHA512 = re.compile(
r'.*/K(?P<domain>[^\s\+]+)\.\+165.+\.private$'
)
def _dyndns_provides(provider, domain):
"""
@ -129,21 +138,22 @@ def dyndns_subscribe(subscribe_host="dyndns.yunohost.org", domain=None, key=None
if key is None:
if len(glob.glob('/etc/yunohost/dyndns/*.key')) == 0:
os.makedirs('/etc/yunohost/dyndns')
if not os.path.exists('/etc/yunohost/dyndns'):
os.makedirs('/etc/yunohost/dyndns')
logger.info(m18n.n('dyndns_key_generating'))
os.system('cd /etc/yunohost/dyndns && '
'dnssec-keygen -a hmac-md5 -b 128 -r /dev/urandom -n USER %s' % domain)
'dnssec-keygen -a hmac-sha512 -b 512 -r /dev/urandom -n USER %s' % domain)
os.system('chmod 600 /etc/yunohost/dyndns/*.key /etc/yunohost/dyndns/*.private')
key_file = glob.glob('/etc/yunohost/dyndns/*.key')[0]
with open(key_file) as f:
key = f.readline().strip().split(' ')[-1]
key = f.readline().strip().split(' ', 6)[-1]
# Send subscription
try:
r = requests.post('https://%s/key/%s' % (subscribe_host, base64.b64encode(key)), data={'subdomain': domain})
r = requests.post('https://%s/key/%s?key_algo=hmac-sha512' % (subscribe_host, base64.b64encode(key)), data={'subdomain': domain}, timeout=30)
except requests.ConnectionError:
raise MoulinetteError(errno.ENETUNREACH, m18n.n('no_internet_connection'))
if r.status_code != 201:
@ -213,6 +223,19 @@ def dyndns_update(dyn_host="dyndns.yunohost.org", domain=None, key=None,
key = keys[0]
# This mean that hmac-md5 is used
# (Re?)Trigger the migration to sha256 and return immediately.
# The actual update will be done in next run.
if "+157" in key:
from yunohost.tools import _get_migration_by_name
migration = _get_migration_by_name("migrate_to_tsig_sha256")
try:
migration["module"].MyMigration().migrate(dyn_host, domain, key)
except Exception as e:
logger.error(m18n.n('migrations_migration_has_failed', exception=e, **migration), exc_info=1)
return
# Extract 'host', e.g. 'nohost.me' from 'foo.nohost.me'
host = domain.split('.')[1:]
host = '.'.join(host)
@ -313,15 +336,13 @@ def _guess_current_dyndns_domain(dyn_host):
dynette...)
"""
re_dyndns_private_key = re.compile(
r'.*/K(?P<domain>[^\s\+]+)\.\+157.+\.private$'
)
# Retrieve the first registered domain
for path in glob.iglob('/etc/yunohost/dyndns/K*.private'):
match = re_dyndns_private_key.match(path)
match = RE_DYNDNS_PRIVATE_KEY_MD5.match(path)
if not match:
continue
match = RE_DYNDNS_PRIVATE_KEY_SHA512.match(path)
if not match:
continue
_domain = match.group('domain')
# Verify if domain is registered (i.e., if it's available, skip

View file

@ -774,30 +774,10 @@ def tools_migrations_migrate(target=None, skip=False):
# loading all migrations
for migration in tools_migrations_list()["migrations"]:
logger.debug(m18n.n('migrations_loading_migration',
number=migration["number"],
name=migration["name"],
))
try:
# this is python builtin method to import a module using a name, we
# use that to import the migration as a python object so we'll be
# able to run it in the next loop
module = import_module("yunohost.data_migrations.{file_name}".format(**migration))
except Exception:
import traceback
traceback.print_exc()
raise MoulinetteError(errno.EINVAL, m18n.n('migrations_error_failed_to_load_migration',
number=migration["number"],
name=migration["name"],
))
break
migrations.append({
"number": migration["number"],
"name": migration["name"],
"module": module,
"module": _get_migration_module(migration),
})
migrations = sorted(migrations, key=lambda x: x["number"])
@ -934,6 +914,47 @@ def _get_migrations_list():
return sorted(migrations)
def _get_migration_by_name(migration_name, with_module=True):
"""
Low-level / "private" function to find a migration by its name
"""
migrations = tools_migrations_list()["migrations"]
matches = [ m for m in migrations if m["name"] == migration_name ]
assert len(matches) == 1, "Unable to find migration with name %s" % migration_name
migration = matches[0]
if with_module:
migration["module"] = _get_migration_module(migration)
return migration
def _get_migration_module(migration):
logger.debug(m18n.n('migrations_loading_migration',
number=migration["number"],
name=migration["name"],
))
try:
# this is python builtin method to import a module using a name, we
# use that to import the migration as a python object so we'll be
# able to run it in the next loop
return import_module("yunohost.data_migrations.{file_name}".format(**migration))
except Exception:
import traceback
traceback.print_exc()
raise MoulinetteError(errno.EINVAL, m18n.n('migrations_error_failed_to_load_migration',
number=migration["number"],
name=migration["name"],
))
class Migration(object):
def migrate(self):