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 <mbasti@redhat.com>
Reviewed-By: Petr Spacek <pspacek@redhat.com>
This commit is contained in:
Petr Spacek 2016-12-21 15:07:34 +01:00 committed by Martin Basti
parent 8db5b277a0
commit fb7c111ac1
11 changed files with 104 additions and 57 deletions

View File

@ -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)

View File

@ -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:

View File

@ -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)

View File

@ -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')

View File

@ -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'))

View File

@ -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)

View File

@ -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.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'])

View File

@ -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:

View File

@ -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'])

View File

@ -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'])

View File

@ -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')