From fb7c111ac13510609e2cba14ecf88cd2ed291a4b Mon Sep 17 00:00:00 2001 From: Petr Spacek Date: Jan 06 2017 08:26:56 +0000 Subject: 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 Reviewed-By: Martin Basti Reviewed-By: Petr Spacek --- diff --git a/ipaclient/install/client.py b/ipaclient/install/client.py index 60a5c18..2ff6122 100644 --- a/ipaclient/install/client.py +++ b/ipaclient/install/client.py @@ -2296,7 +2296,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 f061e79..408ca3f 100644 --- a/ipapython/ipautil.py +++ b/ipapython/ipautil.py @@ -23,6 +23,7 @@ import string import tempfile import subprocess import random +import math import os import sys import copy @@ -51,8 +52,8 @@ from six.moves import urllib 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 PROTOCOL_NAMES = { @@ -789,34 +790,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 414a716..85c2d06 100644 --- a/ipaserver/install/certs.py +++ b/ipaserver/install/certs.py @@ -173,7 +173,7 @@ class CertDB(object): 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 @@ class CertDB(object): 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 @@ class DNSKeySyncInstance(service.Service): 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 @@ class DogtagInstance(service.Service): 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 e8c706e..bacd5fc 100644 --- a/ipaserver/install/httpinstance.py +++ b/ipaserver/install/httpinstance.py @@ -313,7 +313,7 @@ class HTTPInstance(service.Service): 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 . -import string - import six from ipalib import api, errors @@ -35,7 +33,7 @@ from ipalib.request import context 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 @@ UPG_DEFINITION_DN = DN(('cn', 'UPG Definition'), ('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 @@ class baseuser_mod(LDAPUpdate): 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 ipalib.util import (normalize_sshpubkey, validate_sshpubkey_no_options, 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 @@ EXAMPLES: 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 @@ class host_add(LDAPCreate): 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 @@ class host_mod(LDAPUpdate): 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 @@ from .baseuser import ( baseuser_find, baseuser_show, NO_UPG_MAGIC, - baseuser_pwdchars, baseuser_output_params, baseuser_add_manager, baseuser_remove_manager) @@ -47,7 +46,7 @@ from ipalib.util import set_krbcanonicalname 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 @@ class stageuser_add(baseuser_add): # 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 @@ from .baseuser import ( 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 _, ngettext 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 @@ class user_add(baseuser_add): 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 @@ class NSSCertDB(DBMAPHandler): 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')