URL: https://github.com/freeipa/freeipa/pull/317 Author: stlaz Title: #317: Unify password generation across FreeIPA Action: synchronized
To pull the PR as Git branch: git remote add ghfreeipa https://github.com/freeipa/freeipa git fetch ghfreeipa pull/317/head:pr317 git checkout pr317
From bfde1323888d15bd8aa975e9513fea829cb19de9 Mon Sep 17 00:00:00 2001 From: Stanislav Laznicka <slazn...@redhat.com> Date: Tue, 6 Dec 2016 09:05:42 +0100 Subject: [PATCH 1/2] Unify password generation across FreeIPA Also had to recalculate entropy of the passwords as originally, probability of generating each character was 1/256, however the default probability of each character in the ipa_generate_password is 1/95 (1/94 for first and last character). https://fedorahosted.org/freeipa/ticket/5695 --- ipaserver/install/certs.py | 8 ++------ ipaserver/install/dogtaginstance.py | 3 +-- ipaserver/install/dsinstance.py | 5 +---- ipaserver/install/httpinstance.py | 5 ++--- ipaserver/install/server/replicainstall.py | 3 +-- ipaserver/secrets/store.py | 2 +- 6 files changed, 8 insertions(+), 18 deletions(-) diff --git a/ipaserver/install/certs.py b/ipaserver/install/certs.py index 45602ba..198c43d 100644 --- a/ipaserver/install/certs.py +++ b/ipaserver/install/certs.py @@ -25,7 +25,6 @@ import xml.dom.minidom import pwd import base64 -from hashlib import sha1 import fcntl import time import datetime @@ -159,9 +158,6 @@ def set_perms(self, fname, write=False, uid=None): perms |= stat.S_IWUSR os.chmod(fname, perms) - def gen_password(self): - return sha1(ipautil.ipa_generate_password()).hexdigest() - def run_certutil(self, args, stdin=None, **kwargs): return self.nssdb.run_certutil(args, stdin, **kwargs) @@ -177,7 +173,7 @@ def create_noise_file(self): if ipautil.file_exists(self.noise_fname): os.remove(self.noise_fname) f = open(self.noise_fname, "w") - f.write(self.gen_password()) + f.write(ipautil.ipa_generate_password(pwd_len=25)) self.set_perms(self.noise_fname) def create_passwd_file(self, passwd=None): @@ -186,7 +182,7 @@ def create_passwd_file(self, passwd=None): if passwd is not None: f.write("%s\n" % passwd) else: - f.write(self.gen_password()) + f.write(ipautil.ipa_generate_password(pwd_len=25)) f.close() self.set_perms(self.passwd_fname) diff --git a/ipaserver/install/dogtaginstance.py b/ipaserver/install/dogtaginstance.py index f4856c7..dc4b5b0 100644 --- a/ipaserver/install/dogtaginstance.py +++ b/ipaserver/install/dogtaginstance.py @@ -18,7 +18,6 @@ # import base64 -import binascii import ldap import os import shutil @@ -428,7 +427,7 @@ def __add_admin_to_group(self, group): def setup_admin(self): self.admin_user = "admin-%s" % self.fqdn - self.admin_password = binascii.hexlify(os.urandom(16)) + self.admin_password = ipautil.ipa_generate_password(pwd_len=20) self.admin_dn = DN(('uid', self.admin_user), ('ou', 'people'), ('o', 'ipaca')) diff --git a/ipaserver/install/dsinstance.py b/ipaserver/install/dsinstance.py index 1be5ac7..09708dc 100644 --- a/ipaserver/install/dsinstance.py +++ b/ipaserver/install/dsinstance.py @@ -506,7 +506,7 @@ def __setup_sub_dict(self): idrange_size = None self.sub_dict = dict(FQDN=self.fqdn, SERVERID=self.serverid, PASSWORD=self.dm_password, - RANDOM_PASSWORD=self.generate_random(), + RANDOM_PASSWORD=ipautil.ipa_generate_password(), SUFFIX=self.suffix, REALM=self.realm, USER=DS_USER, SERVER_ROOT=server_root, DOMAIN=self.domain, @@ -773,9 +773,6 @@ def __host_nis_groups(self): def __add_enrollment_module(self): self._ldap_mod("enrollment-conf.ldif", self.sub_dict) - def generate_random(self): - return ipautil.ipa_generate_password() - def __enable_ssl(self): dirname = config_dirname(self.serverid) dsdb = certs.CertDB(self.realm, nssdir=dirname, subject_base=self.subject_base) diff --git a/ipaserver/install/httpinstance.py b/ipaserver/install/httpinstance.py index 15c3107..9fdb5a8 100644 --- a/ipaserver/install/httpinstance.py +++ b/ipaserver/install/httpinstance.py @@ -19,7 +19,6 @@ from __future__ import print_function -import binascii import os import os.path import pwd @@ -314,9 +313,9 @@ def create_cert_db(self): ipautil.backup_file(nss_path) # Create the password file for this db - hex_str = binascii.hexlify(os.urandom(10)) + password = ipautil.ipa_generate_password(pwd_len=15) f = os.open(pwd_file, os.O_CREAT | os.O_RDWR) - os.write(f, hex_str) + os.write(f, password) os.close(f) ipautil.run([paths.CERTUTIL, "-d", database, "-f", pwd_file, "-N"]) diff --git a/ipaserver/install/server/replicainstall.py b/ipaserver/install/server/replicainstall.py index f1f7b1b..1d74faa 100644 --- a/ipaserver/install/server/replicainstall.py +++ b/ipaserver/install/server/replicainstall.py @@ -45,7 +45,6 @@ ReplicationManager, replica_conn_check) import SSSDConfig from subprocess import CalledProcessError -from binascii import hexlify if six.PY3: unicode = str @@ -1301,7 +1300,7 @@ def install(installer): if conn.isconnected(): conn.disconnect() os.environ['KRB5CCNAME'] = ccache - config.dirman_password = hexlify(ipautil.ipa_generate_password()) + config.dirman_password = ipautil.ipa_generate_password() # FIXME: allow to use passed in certs instead if ca_enabled: diff --git a/ipaserver/secrets/store.py b/ipaserver/secrets/store.py index 1df7191..1c369d8 100644 --- a/ipaserver/secrets/store.py +++ b/ipaserver/secrets/store.py @@ -122,7 +122,7 @@ def export_key(self): with open(nsspwfile, 'w+') as f: f.write(self.nssdb_password) pk12pwfile = os.path.join(tdir, 'pk12pwfile') - password = b64encode(os.urandom(16)) + password = ipautil.ipa_generate_password(pwd_len=20) with open(pk12pwfile, 'w+') as f: f.write(password) pk12file = os.path.join(tdir, 'pk12file') From 17b08d51708cfdd6e64303c664b7e8bd4c296852 Mon Sep 17 00:00:00 2001 From: Petr Spacek <pspa...@redhat.com> Date: Wed, 21 Dec 2016 15:07:34 +0100 Subject: [PATCH 2/2] ipa_generate_password algorithm change A change to the algorithm that generates random passwords for multiple purposes throught IPA. This spells out the need to assess password strength by the entropy it contains rather than its length. This new password generation should also be compatible with the NSS implementation of password requirements in FIPS environment so that newly created databases won't fail with wrong authentication. https://fedorahosted.org/freeipa/ticket/5695 --- ipaclient/install/client.py | 2 +- ipapython/ipautil.py | 116 +++++++++++++++++++++++--------- ipaserver/install/certs.py | 4 +- ipaserver/install/dnskeysyncinstance.py | 7 +- ipaserver/install/dogtaginstance.py | 2 +- ipaserver/install/httpinstance.py | 2 +- ipaserver/plugins/baseuser.py | 8 +-- ipaserver/plugins/host.py | 12 ++-- ipaserver/plugins/stageuser.py | 5 +- ipaserver/plugins/user.py | 5 +- ipaserver/secrets/store.py | 2 +- 11 files changed, 106 insertions(+), 59 deletions(-) diff --git a/ipaclient/install/client.py b/ipaclient/install/client.py index 0954c2b..2b08f7b 100644 --- a/ipaclient/install/client.py +++ b/ipaclient/install/client.py @@ -2303,7 +2303,7 @@ def create_ipa_nssdb(): ipautil.backup_file(os.path.join(db.secdir, 'secmod.db')) with open(pwdfile, 'w') as f: - f.write(ipautil.ipa_generate_password(pwd_len=40)) + f.write(ipautil.ipa_generate_password()) os.chmod(pwdfile, 0o600) db.create_db(pwdfile) diff --git a/ipapython/ipautil.py b/ipapython/ipautil.py index f85fa0d..a790bf4 100644 --- a/ipapython/ipautil.py +++ b/ipapython/ipautil.py @@ -23,6 +23,7 @@ import tempfile import subprocess import random +import math import os import sys import copy @@ -51,8 +52,8 @@ from ipapython.ipa_log_manager import root_logger from ipapython.dn import DN -GEN_PWD_LEN = 22 -GEN_TMP_PWD_LEN = 12 # only for OTP password that is manually retyped by user +# only for OTP password that is manually retyped by user +TMP_PWD_ENTROPY_BITS = 128 class UnsafeIPAddress(netaddr.IPAddress): @@ -783,34 +784,89 @@ def parse_generalized_time(timestr): except ValueError: return None -def ipa_generate_password(characters=None,pwd_len=None): - ''' Generates password. Password cannot start or end with a whitespace - character. It also cannot be formed by whitespace characters only. - Length of password as well as string of characters to be used by - generator could be optionaly specified by characters and pwd_len - parameters, otherwise default values will be used: characters string - will be formed by all printable non-whitespace characters and space, - pwd_len will be equal to value of GEN_PWD_LEN. - ''' - if not characters: - characters=string.digits + string.ascii_letters + string.punctuation + ' ' - else: - if characters.isspace(): - raise ValueError("password cannot be formed by whitespaces only") - if not pwd_len: - pwd_len = GEN_PWD_LEN - - upper_bound = len(characters) - 1 - rndpwd = '' - r = random.SystemRandom() - - for x in range(pwd_len): - rndchar = characters[r.randint(0,upper_bound)] - if (x == 0) or (x == pwd_len-1): - while rndchar.isspace(): - rndchar = characters[r.randint(0,upper_bound)] - rndpwd += rndchar - return rndpwd + +def ipa_generate_password(entropy_bits=256, uppercase=1, lowercase=1, digits=1, + special=1, min_len=0): + """ + Generate token containing at least `entropy_bits` bits and with the given + character restraints. + + :param entropy_bits: + The minimal number of entropy bits attacker has to guess: + 128 bits entropy: secure + 256 bits of entropy: secure enough if you care about quantum + computers + + Integer values specify minimal number of characters from given + character class and length. + Value None prevents given character from appearing in the token. + + Example: + TokenGenerator(uppercase=3, lowercase=3, digits=0, special=None) + + At least 3 upper and 3 lower case ASCII chars, may contain digits, + no special chars. + """ + special_chars = '!$%&()*+,-./:;<>?@[]^_{|}~' + pwd_charsets = { + 'uppercase': { + 'chars': string.ascii_uppercase, + 'entropy': math.log(len(string.ascii_uppercase), 2) + }, + 'lowercase': { + 'chars': string.ascii_lowercase, + 'entropy': math.log(len(string.ascii_lowercase), 2) + }, + 'digits': { + 'chars': string.digits, + 'entropy': math.log(len(string.digits), 2) + }, + 'special': { + 'chars': special_chars, + 'entropy': math.log(len(special_chars), 2) + }, + } + req_classes = dict( + uppercase=uppercase, + lowercase=lowercase, + digits=digits, + special=special + ) + # 'all' class is used when adding entropy to too-short tokens + # it contains characters from all allowed classes + pwd_charsets['all'] = { + 'chars': ''.join([ + charclass['chars'] for charclass_name, charclass + in pwd_charsets.items() + if req_classes[charclass_name] is not None + ]) + } + pwd_charsets['all']['entropy'] = math.log( + len(pwd_charsets['all']['chars']), 2) + rnd = random.SystemRandom() + + todo_entropy = entropy_bits + password = '' + # Generate required character classes: + # The order of generated characters is fixed to comply with check in + # NSS function sftk_newPinCheck() in nss/lib/softoken/fipstokn.c. + for charclass_name in ['digits', 'uppercase', 'lowercase', 'special']: + charclass = pwd_charsets[charclass_name] + todo_characters = req_classes[charclass_name] + while todo_characters > 0: + password += rnd.choice(charclass['chars']) + todo_entropy -= charclass['entropy'] + todo_characters -= 1 + + # required character classes do not provide sufficient entropy + # or does not fulfill minimal length constraint + allchars = pwd_charsets['all'] + while todo_entropy > 0 or len(password) < min_len: + password += rnd.choice(allchars['chars']) + todo_entropy -= allchars['entropy'] + + return password + def user_input(prompt, default = None, allow_empty = True): if default == None: diff --git a/ipaserver/install/certs.py b/ipaserver/install/certs.py index 198c43d..8673a48 100644 --- a/ipaserver/install/certs.py +++ b/ipaserver/install/certs.py @@ -173,7 +173,7 @@ def create_noise_file(self): if ipautil.file_exists(self.noise_fname): os.remove(self.noise_fname) f = open(self.noise_fname, "w") - f.write(ipautil.ipa_generate_password(pwd_len=25)) + f.write(ipautil.ipa_generate_password()) self.set_perms(self.noise_fname) def create_passwd_file(self, passwd=None): @@ -182,7 +182,7 @@ def create_passwd_file(self, passwd=None): if passwd is not None: f.write("%s\n" % passwd) else: - f.write(ipautil.ipa_generate_password(pwd_len=25)) + f.write(ipautil.ipa_generate_password()) f.close() self.set_perms(self.passwd_fname) diff --git a/ipaserver/install/dnskeysyncinstance.py b/ipaserver/install/dnskeysyncinstance.py index 76a14f9..861a170 100644 --- a/ipaserver/install/dnskeysyncinstance.py +++ b/ipaserver/install/dnskeysyncinstance.py @@ -224,10 +224,11 @@ def __setup_softhsm(self): os.chown(paths.DNSSEC_TOKENS_DIR, self.ods_uid, self.named_gid) # generate PINs for softhsm - allowed_chars = u'123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' pin_length = 30 # Bind allows max 32 bytes including ending '\0' - pin = ipautil.ipa_generate_password(allowed_chars, pin_length) - pin_so = ipautil.ipa_generate_password(allowed_chars, pin_length) + pin = ipautil.ipa_generate_password( + entropy_bits=0, special=None, min_len=pin_length) + pin_so = ipautil.ipa_generate_password( + entropy_bits=0, special=None, min_len=pin_length) self.logger.debug("Saving user PIN to %s", paths.DNSSEC_SOFTHSM_PIN) named_fd = open(paths.DNSSEC_SOFTHSM_PIN, 'w') diff --git a/ipaserver/install/dogtaginstance.py b/ipaserver/install/dogtaginstance.py index dc4b5b0..c3c470d 100644 --- a/ipaserver/install/dogtaginstance.py +++ b/ipaserver/install/dogtaginstance.py @@ -427,7 +427,7 @@ def __add_admin_to_group(self, group): def setup_admin(self): self.admin_user = "admin-%s" % self.fqdn - self.admin_password = ipautil.ipa_generate_password(pwd_len=20) + self.admin_password = ipautil.ipa_generate_password() self.admin_dn = DN(('uid', self.admin_user), ('ou', 'people'), ('o', 'ipaca')) diff --git a/ipaserver/install/httpinstance.py b/ipaserver/install/httpinstance.py index 9fdb5a8..35ed91c 100644 --- a/ipaserver/install/httpinstance.py +++ b/ipaserver/install/httpinstance.py @@ -313,7 +313,7 @@ def create_cert_db(self): ipautil.backup_file(nss_path) # Create the password file for this db - password = ipautil.ipa_generate_password(pwd_len=15) + password = ipautil.ipa_generate_password() f = os.open(pwd_file, os.O_CREAT | os.O_RDWR) os.write(f, password) os.close(f) diff --git a/ipaserver/plugins/baseuser.py b/ipaserver/plugins/baseuser.py index 4c7e9f0..85ad417 100644 --- a/ipaserver/plugins/baseuser.py +++ b/ipaserver/plugins/baseuser.py @@ -17,8 +17,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -import string - import six from ipalib import api, errors @@ -35,7 +33,7 @@ from ipalib import _ from ipalib.constants import PATTERN_GROUPUSER_NAME from ipapython import kerberos -from ipapython.ipautil import ipa_generate_password, GEN_TMP_PWD_LEN +from ipapython.ipautil import ipa_generate_password, TMP_PWD_ENTROPY_BITS from ipapython.ipavalidate import Email from ipalib.util import ( normalize_sshpubkey, @@ -75,8 +73,6 @@ ('cn', 'etc'), api.env.basedn) -# characters to be used for generating random user passwords -baseuser_pwdchars = string.digits + string.ascii_letters + '_,.@+-=' def validate_nsaccountlock(entry_attrs): if 'nsaccountlock' in entry_attrs: @@ -554,7 +550,7 @@ def check_manager(self, entry_attrs, container): def check_userpassword(self, entry_attrs, **options): if 'userpassword' not in entry_attrs and options.get('random'): entry_attrs['userpassword'] = ipa_generate_password( - baseuser_pwdchars, pwd_len=GEN_TMP_PWD_LEN) + entropy_bits=TMP_PWD_ENTROPY_BITS) # save the password so it can be displayed in post_callback setattr(context, 'randompassword', entry_attrs['userpassword']) diff --git a/ipaserver/plugins/host.py b/ipaserver/plugins/host.py index 957a1ed..58e711f 100644 --- a/ipaserver/plugins/host.py +++ b/ipaserver/plugins/host.py @@ -21,7 +21,6 @@ from __future__ import absolute_import import dns.resolver -import string import six @@ -62,7 +61,7 @@ from ipapython.ipautil import ( ipa_generate_password, CheckedIPAddress, - GEN_TMP_PWD_LEN + TMP_PWD_ENTROPY_BITS ) from ipapython.dnsutil import DNSName from ipapython.ssh import SSHPublicKey @@ -136,10 +135,6 @@ register = Registry() -# Characters to be used by random password generator -# The set was chosen to avoid the need for escaping the characters by user -host_pwd_chars = string.digits + string.ascii_letters + '_,.@+-=' - def remove_ptr_rec(ipaddr, fqdn): """ @@ -688,7 +683,7 @@ def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options): entry_attrs['objectclass'].remove('krbprincipal') if options.get('random'): entry_attrs['userpassword'] = ipa_generate_password( - characters=host_pwd_chars, pwd_len=GEN_TMP_PWD_LEN) + entropy_bits=TMP_PWD_ENTROPY_BITS) # save the password so it can be displayed in post_callback setattr(context, 'randompassword', entry_attrs['userpassword']) certs = options.get('usercertificate', []) @@ -915,7 +910,8 @@ def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options): entry_attrs['usercertificate'] = certs_der if options.get('random'): - entry_attrs['userpassword'] = ipa_generate_password(characters=host_pwd_chars) + entry_attrs['userpassword'] = ipa_generate_password( + entropy_bits=TMP_PWD_ENTROPY_BITS) setattr(context, 'randompassword', entry_attrs['userpassword']) if 'macaddress' in entry_attrs: diff --git a/ipaserver/plugins/stageuser.py b/ipaserver/plugins/stageuser.py index 1da43ec..afd402e 100644 --- a/ipaserver/plugins/stageuser.py +++ b/ipaserver/plugins/stageuser.py @@ -38,7 +38,6 @@ baseuser_find, baseuser_show, NO_UPG_MAGIC, - baseuser_pwdchars, baseuser_output_params, baseuser_add_manager, baseuser_remove_manager) @@ -47,7 +46,7 @@ from ipalib import _, ngettext from ipalib import output from ipaplatform.paths import paths -from ipapython.ipautil import ipa_generate_password, GEN_TMP_PWD_LEN +from ipapython.ipautil import ipa_generate_password, TMP_PWD_ENTROPY_BITS from ipalib.capabilities import client_has_capability if six.PY3: @@ -340,7 +339,7 @@ def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options): # If requested, generate a userpassword if 'userpassword' not in entry_attrs and options.get('random'): entry_attrs['userpassword'] = ipa_generate_password( - baseuser_pwdchars, pwd_len=GEN_TMP_PWD_LEN) + entropy_bits=TMP_PWD_ENTROPY_BITS) # save the password so it can be displayed in post_callback setattr(context, 'randompassword', entry_attrs['userpassword']) diff --git a/ipaserver/plugins/user.py b/ipaserver/plugins/user.py index 5296093..6440548 100644 --- a/ipaserver/plugins/user.py +++ b/ipaserver/plugins/user.py @@ -38,7 +38,6 @@ NO_UPG_MAGIC, UPG_DEFINITION_DN, baseuser_output_params, - baseuser_pwdchars, validate_nsaccountlock, convert_nsaccountlock, fix_addressbook_permission_bindrule, @@ -63,7 +62,7 @@ from ipalib import output from ipaplatform.paths import paths from ipapython.dn import DN -from ipapython.ipautil import ipa_generate_password, GEN_TMP_PWD_LEN +from ipapython.ipautil import ipa_generate_password, TMP_PWD_ENTROPY_BITS from ipalib.capabilities import client_has_capability if api.env.in_server: @@ -529,7 +528,7 @@ def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options): if 'userpassword' not in entry_attrs and options.get('random'): entry_attrs['userpassword'] = ipa_generate_password( - baseuser_pwdchars, pwd_len=GEN_TMP_PWD_LEN) + entropy_bits=TMP_PWD_ENTROPY_BITS) # save the password so it can be displayed in post_callback setattr(context, 'randompassword', entry_attrs['userpassword']) diff --git a/ipaserver/secrets/store.py b/ipaserver/secrets/store.py index 1c369d8..2c58eee 100644 --- a/ipaserver/secrets/store.py +++ b/ipaserver/secrets/store.py @@ -122,7 +122,7 @@ def export_key(self): with open(nsspwfile, 'w+') as f: f.write(self.nssdb_password) pk12pwfile = os.path.join(tdir, 'pk12pwfile') - password = ipautil.ipa_generate_password(pwd_len=20) + password = ipautil.ipa_generate_password() with open(pk12pwfile, 'w+') as f: f.write(password) pk12file = os.path.join(tdir, 'pk12file')
-- Manage your subscription for the Freeipa-devel mailing list: https://www.redhat.com/mailman/listinfo/freeipa-devel Contribute to FreeIPA: http://www.freeipa.org/page/Contribute/Code