From fb7c111ac13510609e2cba14ecf88cd2ed291a4b Mon Sep 17 00:00:00 2001 From: Petr Spacek Date: Wed, 21 Dec 2016 15:07:34 +0100 Subject: [PATCH] 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 --- ipaclient/install/client.py | 2 +- ipapython/ipautil.py | 112 ++++++++++++++++++------ 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, 104 insertions(+), 57 deletions(-) diff --git a/ipaclient/install/client.py b/ipaclient/install/client.py index 60a5c180a..2ff612280 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 f061e7961..408ca3fb0 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() +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 - 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 user_input(prompt, default = None, allow_empty = True): if default == None: diff --git a/ipaserver/install/certs.py b/ipaserver/install/certs.py index 414a71664..85c2d06c0 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 76a14f9d9..861a1702e 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 dc4b5b071..c3c470d62 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 e8c706e7a..bacd5fc4f 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 4c7e9f083..85ad41768 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 957a1edcf..58e711f34 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 1da43ecb6..afd402ea2 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 529609314..64405483a 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 1c369d8cd..2c58eeedb 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')