summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPetr Spacek <pspacek@redhat.com>2016-12-21 15:07:34 +0100
committerMartin Basti <mbasti@redhat.com>2017-01-06 09:26:56 +0100
commitfb7c111ac13510609e2cba14ecf88cd2ed291a4b (patch)
tree3c963ca45514bbd66706a27175726a19a9f87713
parent8db5b277a079fdfe5efbd7d49311f14489cee0e8 (diff)
downloadfreeipa-fb7c111ac13510609e2cba14ecf88cd2ed291a4b.tar.gz
freeipa-fb7c111ac13510609e2cba14ecf88cd2ed291a4b.tar.xz
freeipa-fb7c111ac13510609e2cba14ecf88cd2ed291a4b.zip
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>
-rw-r--r--ipaclient/install/client.py2
-rw-r--r--ipapython/ipautil.py116
-rw-r--r--ipaserver/install/certs.py4
-rw-r--r--ipaserver/install/dnskeysyncinstance.py7
-rw-r--r--ipaserver/install/dogtaginstance.py2
-rw-r--r--ipaserver/install/httpinstance.py2
-rw-r--r--ipaserver/plugins/baseuser.py8
-rw-r--r--ipaserver/plugins/host.py12
-rw-r--r--ipaserver/plugins/stageuser.py5
-rw-r--r--ipaserver/plugins/user.py5
-rw-r--r--ipaserver/secrets/store.py2
11 files changed, 106 insertions, 59 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()
-
- 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 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 <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'])
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')