mirror of
https://github.com/YunoHost/yunohost.git
synced 2024-09-03 20:06:10 +02:00
Merge pull request #372 from YunoHost/tsig-sha256
Uses hmac-sha512 for dyndns TSIG
This commit is contained in:
commit
be8ae067e6
4 changed files with 172 additions and 31 deletions
|
@ -213,6 +213,14 @@
|
||||||
"mailbox_used_space_dovecot_down": "Dovecot mailbox service need to be up, if you want to get mailbox used space",
|
"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_change_failed": "Unable to change the main domain",
|
||||||
"maindomain_changed": "The main domain has been changed",
|
"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_backward": "Migrating backward.",
|
||||||
"migrations_bad_value_for_target": "Invalide number for target argument, available migrations numbers are 0 or {}",
|
"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",
|
"migrations_cant_reach_migration_file": "Can't access migrations files at path %s",
|
||||||
|
|
91
src/yunohost/data_migrations/0002_migrate_to_tsig_sha256.py
Normal file
91
src/yunohost/data_migrations/0002_migrate_to_tsig_sha256.py
Normal 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
|
||||||
|
|
|
@ -27,6 +27,7 @@ import os
|
||||||
import re
|
import re
|
||||||
import json
|
import json
|
||||||
import glob
|
import glob
|
||||||
|
import time
|
||||||
import base64
|
import base64
|
||||||
import errno
|
import errno
|
||||||
import requests
|
import requests
|
||||||
|
@ -46,6 +47,14 @@ OLD_IPV4_FILE = '/etc/yunohost/dyndns/old_ip'
|
||||||
OLD_IPV6_FILE = '/etc/yunohost/dyndns/old_ipv6'
|
OLD_IPV6_FILE = '/etc/yunohost/dyndns/old_ipv6'
|
||||||
DYNDNS_ZONE = '/etc/yunohost/dyndns/zone'
|
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):
|
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 key is None:
|
||||||
if len(glob.glob('/etc/yunohost/dyndns/*.key')) == 0:
|
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'))
|
logger.info(m18n.n('dyndns_key_generating'))
|
||||||
|
|
||||||
os.system('cd /etc/yunohost/dyndns && '
|
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')
|
os.system('chmod 600 /etc/yunohost/dyndns/*.key /etc/yunohost/dyndns/*.private')
|
||||||
|
|
||||||
key_file = glob.glob('/etc/yunohost/dyndns/*.key')[0]
|
key_file = glob.glob('/etc/yunohost/dyndns/*.key')[0]
|
||||||
with open(key_file) as f:
|
with open(key_file) as f:
|
||||||
key = f.readline().strip().split(' ')[-1]
|
key = f.readline().strip().split(' ', 6)[-1]
|
||||||
|
|
||||||
# Send subscription
|
# Send subscription
|
||||||
try:
|
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:
|
except requests.ConnectionError:
|
||||||
raise MoulinetteError(errno.ENETUNREACH, m18n.n('no_internet_connection'))
|
raise MoulinetteError(errno.ENETUNREACH, m18n.n('no_internet_connection'))
|
||||||
if r.status_code != 201:
|
if r.status_code != 201:
|
||||||
|
@ -213,6 +223,19 @@ def dyndns_update(dyn_host="dyndns.yunohost.org", domain=None, key=None,
|
||||||
|
|
||||||
key = keys[0]
|
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'
|
# Extract 'host', e.g. 'nohost.me' from 'foo.nohost.me'
|
||||||
host = domain.split('.')[1:]
|
host = domain.split('.')[1:]
|
||||||
host = '.'.join(host)
|
host = '.'.join(host)
|
||||||
|
@ -313,15 +336,13 @@ def _guess_current_dyndns_domain(dyn_host):
|
||||||
dynette...)
|
dynette...)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
re_dyndns_private_key = re.compile(
|
|
||||||
r'.*/K(?P<domain>[^\s\+]+)\.\+157.+\.private$'
|
|
||||||
)
|
|
||||||
|
|
||||||
# Retrieve the first registered domain
|
# Retrieve the first registered domain
|
||||||
for path in glob.iglob('/etc/yunohost/dyndns/K*.private'):
|
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:
|
if not match:
|
||||||
continue
|
match = RE_DYNDNS_PRIVATE_KEY_SHA512.match(path)
|
||||||
|
if not match:
|
||||||
|
continue
|
||||||
_domain = match.group('domain')
|
_domain = match.group('domain')
|
||||||
|
|
||||||
# Verify if domain is registered (i.e., if it's available, skip
|
# Verify if domain is registered (i.e., if it's available, skip
|
||||||
|
|
|
@ -774,30 +774,10 @@ def tools_migrations_migrate(target=None, skip=False):
|
||||||
|
|
||||||
# loading all migrations
|
# loading all migrations
|
||||||
for migration in tools_migrations_list()["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({
|
migrations.append({
|
||||||
"number": migration["number"],
|
"number": migration["number"],
|
||||||
"name": migration["name"],
|
"name": migration["name"],
|
||||||
"module": module,
|
"module": _get_migration_module(migration),
|
||||||
})
|
})
|
||||||
|
|
||||||
migrations = sorted(migrations, key=lambda x: x["number"])
|
migrations = sorted(migrations, key=lambda x: x["number"])
|
||||||
|
@ -934,6 +914,47 @@ def _get_migrations_list():
|
||||||
return sorted(migrations)
|
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):
|
class Migration(object):
|
||||||
|
|
||||||
def migrate(self):
|
def migrate(self):
|
||||||
|
|
Loading…
Add table
Reference in a new issue