[Pkg-freeipa-devel] freeipa: Changes to 'upstream-unstable'

Timo Aaltonen tjaalton-guest at alioth.debian.org
Mon Feb 11 22:07:16 UTC 2013


Rebased ref, commits from common ancestor:
commit 64de33a772255e6c8a921a6463ca1c751e25512d
Author: Rob Crittenden <rcritten at redhat.com>
Date:   Wed Jan 23 15:32:16 2013 -0500

    Become IPA 3.1.2

diff --git a/VERSION b/VERSION
index 5cc5ec1..72d06f6 100644
--- a/VERSION
+++ b/VERSION
@@ -20,7 +20,7 @@
 ########################################################
 IPA_VERSION_MAJOR=3
 IPA_VERSION_MINOR=1
-IPA_VERSION_RELEASE=1
+IPA_VERSION_RELEASE=2
 
 ########################################################
 # For 'pre' releases the version will be               #

commit d764dbabbd8c9ae1ff984424ddf613932923c766
Author: Rob Crittenden <rcritten at redhat.com>
Date:   Tue Jan 22 17:06:04 2013 -0500

    Update anonymous access ACI to protect secret attributes.
    
    Update anonymous access ACI so that no users besides Trust Admins
    users can read AD Trust key attributes (ipaNTTrustAuthOutgoing,
    ipaNTTrustAuthIncoming). The change is applied both for updated
    IPA servers and new installations.

diff --git a/install/share/default-aci.ldif b/install/share/default-aci.ldif
index f3ed395..3e6c100 100644
--- a/install/share/default-aci.ldif
+++ b/install/share/default-aci.ldif
@@ -3,7 +3,7 @@
 dn: $SUFFIX
 changetype: modify
 add: aci
-aci: (target != "ldap:///idnsname=*,cn=dns,$SUFFIX")(targetattr != "userPassword || krbPrincipalKey || sambaLMPassword || sambaNTPassword || passwordHistory || krbMKey || userPKCS12 || ipaNTHash")(version 3.0; acl "Enable Anonymous access"; allow (read, search, compare) userdn = "ldap:///anyone";)
+aci: (target != "ldap:///idnsname=*,cn=dns,$SUFFIX")(targetattr != "userPassword || krbPrincipalKey || sambaLMPassword || sambaNTPassword || passwordHistory || krbMKey || userPKCS12 || ipaNTHash || ipaNTTrustAuthOutgoing || ipaNTTrustAuthIncoming")(version 3.0; acl "Enable Anonymous access"; allow (read, search, compare) userdn = "ldap:///anyone";)
 aci: (targetattr = "memberOf || memberHost || memberUser")(version 3.0; acl "No anonymous access to member information"; deny (read,search,compare) userdn != "ldap:///all";)
 aci: (targetattr != "userPassword || krbPrincipalKey || sambaLMPassword || sambaNTPassword || passwordHistory || krbMKey || krbPrincipalName || krbCanonicalName || krbUPEnabled || krbTicketPolicyReference || krbPrincipalExpiration || krbPasswordExpiration || krbPwdPolicyReference || krbPrincipalType || krbPwdHistory || krbLastPwdChange || krbPrincipalAliases || krbExtraData || krbLastSuccessfulAuth || krbLastFailedAuth || krbLoginFailedCount || krbTicketFlags || ipaUniqueId || memberOf || serverHostName || enrolledBy || ipaNTHash")(version 3.0; acl "Admin can manage any entry"; allow (all) groupdn = "ldap:///cn=admins,cn=groups,cn=accounts,$SUFFIX";)
 aci: (targetattr = "userpassword || krbprincipalkey || sambalmpassword || sambantpassword")(version 3.0; acl "selfservice:Self can write own password"; allow (write) userdn="ldap:///self";)
diff --git a/ipaserver/install/plugins/Makefile.am b/ipaserver/install/plugins/Makefile.am
index d29103a..a0c62ca 100644
--- a/ipaserver/install/plugins/Makefile.am
+++ b/ipaserver/install/plugins/Makefile.am
@@ -9,6 +9,7 @@ app_PYTHON = 			\
 	dns.py			\
 	updateclient.py		\
 	update_services.py	\
+	update_anonymous_aci.py	\
 	$(NULL)
 
 EXTRA_DIST =			\
diff --git a/ipaserver/install/plugins/update_anonymous_aci.py b/ipaserver/install/plugins/update_anonymous_aci.py
new file mode 100644
index 0000000..2b7446a
--- /dev/null
+++ b/ipaserver/install/plugins/update_anonymous_aci.py
@@ -0,0 +1,81 @@
+# Authors:
+#   Rob Crittenden <rcritten at redhat.com>
+#
+# Copyright (C) 2013  Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+from copy import deepcopy
+from ipaserver.install.plugins import FIRST, LAST
+from ipaserver.install.plugins.baseupdate import PostUpdate
+#from ipalib.frontend import Updater
+#from ipaserver.install.plugins import baseupdate
+from ipalib import api
+from ipalib.aci import ACI
+from ipalib.plugins import aci
+from ipapython.ipa_log_manager import *
+
+class update_anonymous_aci(PostUpdate):
+    """
+    Update the Anonymous ACI to ensure that all secrets are protected.
+    """
+    order = FIRST
+
+    def execute(self, **options):
+        aciname = u'Enable Anonymous access'
+        aciprefix = u'none'
+        ldap = self.obj.backend
+
+        (dn, entry_attrs) = ldap.get_entry(api.env.basedn, ['aci'])
+
+        acistrs = entry_attrs.get('aci', [])
+        acilist = aci._convert_strings_to_acis(entry_attrs.get('aci', []))
+        rawaci = aci._find_aci_by_name(acilist, aciprefix, aciname)
+
+        attrs = rawaci.target['targetattr']['expression']
+
+        update_attrs = deepcopy(attrs)
+
+        needed_attrs = []
+        for attr in ('ipaNTTrustAuthOutgoing', 'ipaNTTrustAuthIncoming'):
+            if attr not in attrs:
+                needed_attrs.append(attr)
+
+        update_attrs.extend(needed_attrs)
+        if len(attrs) == len(update_attrs):
+            root_logger.debug("Anonymous ACI already update-to-date")
+            return (False, False, [])
+        else:
+            root_logger.debug("New Anonymous ACI attributes needed: %s",
+                needed_attrs)
+
+        for tmpaci in acistrs:
+            candidate = ACI(tmpaci)
+            if rawaci.isequal(candidate):
+                acistrs.remove(tmpaci)
+                break
+
+        rawaci.target['targetattr']['expression'] = update_attrs
+        acistrs.append(unicode(rawaci))
+        entry_attrs['aci'] = acistrs
+
+        try:
+            ldap.update_entry(dn, entry_attrs)
+        except Exception, e:
+            root_logger.error("Failed to update Anonymous ACI: %s" % e)
+
+        return (False, False, [])
+
+api.register(update_anonymous_aci)

commit 0a38d9af4e05a5c8e22f25ca39133402aad9948e
Author: Rob Crittenden <rcritten at redhat.com>
Date:   Wed Jan 16 13:20:14 2013 -0500

    Don't initialize NSS if we don't have to, clean up unused cert refs
    
    Check to see if NSS is initialized before trying to do so again.
    
    If we are temporarily creating a certificate be sure to delete it in order
    to remove references to it and avoid NSS shutdown issues.
    
    In the certificate load validator shut down NSS if we end up initializing
    it. I'm not entirely sure why but this prevents a later shutdown issue
    if we are passed the --ca-cert-file option.

diff --git a/ipa-client/ipa-install/ipa-client-install b/ipa-client/ipa-install/ipa-client-install
index bd299f9..f068c9d 100755
--- a/ipa-client/ipa-install/ipa-client-install
+++ b/ipa-client/ipa-install/ipa-client-install
@@ -48,6 +48,7 @@ try:
     from ipapython.dn import DN
     from ipapython.ssh import SSHPublicKey
     from ipalib.rpc import delete_persistent_client_session_data
+    import nss.nss as nss
     import SSSDConfig
     from ConfigParser import RawConfigParser
     from optparse import SUPPRESS_HELP, OptionGroup, OptionValueError
@@ -77,10 +78,15 @@ def parse_options():
         if not os.path.isabs(value):
             raise OptionValueError("%s option '%s' is not an absolute file path" % (opt, value))
 
+        initialized = nss.nss_is_initialized()
         try:
             cert = x509.load_certificate_from_file(value)
         except Exception, e:
             raise OptionValueError("%s option '%s' is not a valid certificate file" % (opt, value))
+        else:
+            del(cert)
+            if not initialized:
+                nss.nss_shutdown()
 
         parser.values.ca_cert_file = value
 
@@ -1372,6 +1378,8 @@ def get_ca_cert_from_file(url):
     except Exception, e:
         raise errors.FileError(reason =
             u"cannot write certificate file '%s': %s" % (CACERT, e))
+    else:
+        del(cert)
 
 def get_ca_cert_from_http(url, ca_file, warn=True):
     '''
@@ -1478,6 +1486,8 @@ def validate_new_ca_cert(existing_ca_cert, ca_file, ask, override=False):
         root_logger.debug(
                 "Existing CA cert and Retrieved CA cert are identical")
         os.remove(ca_file)
+        del(existing_ca_cert)
+        del(new_ca_cert)
 
 
 def get_ca_cert(fstore, options, server, basedn):
diff --git a/ipalib/x509.py b/ipalib/x509.py
index f8a1357..4f81fb5 100644
--- a/ipalib/x509.py
+++ b/ipalib/x509.py
@@ -91,18 +91,18 @@ def load_certificate(data, datatype=PEM, dbdir=None):
         data = strip_header(data)
         data = base64.b64decode(data)
 
-    if dbdir is None:
-        if 'in_tree' in api.env:
-            if api.env.in_tree:
-                dbdir = api.env.dot_ipa + os.sep + 'alias'
+    if not nss.nss_is_initialized():
+        if dbdir is None:
+            if 'in_tree' in api.env:
+                if api.env.in_tree:
+                    dbdir = api.env.dot_ipa + os.sep + 'alias'
+                else:
+                    dbdir = "/etc/httpd/alias"
+                nss.nss_init(dbdir)
             else:
-                dbdir = "/etc/httpd/alias"
-            nss.nss_init(dbdir)
+                nss.nss_init_nodb()
         else:
-            nss.nss_init_nodb()
-    else:
-        nss.nss_init(dbdir)
-
+            nss.nss_init(dbdir)
 
     return nss.Certificate(buffer(data))
 
@@ -139,7 +139,9 @@ def get_subject(certificate, datatype=PEM, dbdir=None):
     """
 
     nsscert = load_certificate(certificate, datatype, dbdir)
-    return nsscert.subject
+    subject = nsscert.subject
+    del(nsscert)
+    return subject
 
 def get_issuer(certificate, datatype=PEM, dbdir=None):
     """
@@ -147,14 +149,18 @@ def get_issuer(certificate, datatype=PEM, dbdir=None):
     """
 
     nsscert = load_certificate(certificate, datatype, dbdir)
-    return nsscert.issuer
+    issuer = nsscert.issuer
+    del(nsscert)
+    return issuer
 
 def get_serial_number(certificate, datatype=PEM, dbdir=None):
     """
     Return the decimal value of the serial number.
     """
     nsscert = load_certificate(certificate, datatype, dbdir)
-    return nsscert.serial_number
+    serial_number = nsscert.serial_number
+    del(nsscert)
+    return serial_number
 
 def make_pem(data):
     """
@@ -230,6 +236,7 @@ def verify_cert_subject(ldap, hostname, dercert):
     nsscert = load_certificate(dercert, datatype=DER)
     subject = str(nsscert.subject)
     issuer = str(nsscert.issuer)
+    del(nsscert)
 
     # Handle both supported forms of issuer, from selfsign and dogtag.
     if (not valid_issuer(issuer)):

commit a9e55ffdf596de72bf8661a020f8afd3f161b182
Author: John Dennis <jdennis at redhat.com>
Date:   Thu Nov 15 14:57:52 2012 -0500

    Use secure method to acquire IPA CA certificate
    
    Major changes ipa-client-install:
    
    * Use GSSAPI connection to LDAP server to download CA cert (now
      the default method)
    
    * Add --ca-cert-file option to load the CA cert from a disk file.
      Validate the file. If this option is used the supplied CA cert
      is considered definitive.
    
    * The insecure HTTP retrieval method is still supported but it must be
      explicitly forced and a warning will be emitted.
    
    * Remain backward compatible with unattended case (except for aberrant
      condition when preexisting /etc/ipa/ca.crt differs from securely
      obtained CA cert, see below)
    
    * If /etc/ipa/ca.crt CA cert preexists the validate it matches the
      securely acquired CA cert, if not:
    
      - If --unattended and not --force abort with error
    
      - If interactive query user to accept new CA cert, if not abort
    
      In either case warn user.
    
    * If interactive and LDAP retrieval fails prompt user if they want to
      proceed with insecure HTTP method
    
    * If not interactive and LDAP retrieval fails abort unless --force
    
    * Backup preexisting /etc/ipa/ca.crt in FileStore prior to execution,
      if ipa-client-install fails it will be restored.
    
    Other changes:
    
    * Add new exception class CertificateInvalidError
    
    * Add utility convert_ldap_error() to ipalib.ipautil
    
    * Replace all hardcoded instances of /etc/ipa/ca.crt in
      ipa-client-install with CACERT constant (matches existing practice
      elsewhere).
    
    * ipadiscovery no longer retrieves CA cert via HTTP.
    
    * Handle LDAP minssf failures during discovery, treat failure to check
      ldap server as a warninbg in absebce of a provided CA certificate via
      --ca-cert-file or though existing /etc/ipa/ca.crt file.
    
    Signed-off-by: Simo Sorce <simo at redhat.com>
    Signed-off-by: Rob Crittenden <rcritten at redhat.com>

diff --git a/ipa-client/ipa-install/ipa-client-install b/ipa-client/ipa-install/ipa-client-install
index a38c828..bd299f9 100755
--- a/ipa-client/ipa-install/ipa-client-install
+++ b/ipa-client/ipa-install/ipa-client-install
@@ -25,14 +25,18 @@ try:
     import os
     import time
     import socket
+    import ldap
+    import ldap.sasl
+    import urlparse
 
     from ipapython.ipa_log_manager import *
     import tempfile
     import getpass
     from ipaclient import ipadiscovery
+    from ipaclient.ipadiscovery import CACERT
     import ipaclient.ipachangeconf
     import ipaclient.ntpconf
-    from ipapython.ipautil import run, user_input, CalledProcessError, file_exists, realm_to_suffix
+    from ipapython.ipautil import run, user_input, CalledProcessError, file_exists, realm_to_suffix, convert_ldap_error
     import ipapython.services as ipaservices
     from ipapython import ipautil
     from ipapython import sysrestore
@@ -40,12 +44,13 @@ try:
     from ipapython import certmonger
     from ipapython.config import IPAOptionParser
     from ipalib import api, errors
+    from ipalib import x509
     from ipapython.dn import DN
     from ipapython.ssh import SSHPublicKey
     from ipalib.rpc import delete_persistent_client_session_data
     import SSSDConfig
     from ConfigParser import RawConfigParser
-    from optparse import SUPPRESS_HELP, OptionGroup
+    from optparse import SUPPRESS_HELP, OptionGroup, OptionValueError
 except ImportError:
     print >> sys.stderr, """\
 There was a problem importing one of the required Python modules. The
@@ -55,6 +60,7 @@ error was:
 """ % sys.exc_value
     sys.exit(1)
 
+SUCCESS = 0
 CLIENT_INSTALL_ERROR = 1
 CLIENT_NOT_CONFIGURED = 2
 CLIENT_ALREADY_CONFIGURED = 3
@@ -63,6 +69,21 @@ CLIENT_UNINSTALL_ERROR = 4 # error after restoring files/state
 client_nss_nickname_format = 'IPA Machine Certificate - %s'
 
 def parse_options():
+    def validate_ca_cert_file_option(option, opt, value, parser):
+        if not os.path.exists(value):
+            raise OptionValueError("%s option '%s' does not exist" % (opt, value))
+        if not os.path.isfile(value):
+            raise OptionValueError("%s option '%s' is not a file" % (opt, value))
+        if not os.path.isabs(value):
+            raise OptionValueError("%s option '%s' is not an absolute file path" % (opt, value))
+
+        try:
+            cert = x509.load_certificate_from_file(value)
+        except Exception, e:
+            raise OptionValueError("%s option '%s' is not a valid certificate file" % (opt, value))
+
+        parser.values.ca_cert_file = value
+
     parser = IPAOptionParser(version=version.VERSION)
 
     basic_group = OptionGroup(parser, "basic options")
@@ -108,6 +129,9 @@ def parse_options():
     basic_group.add_option("-U", "--unattended", dest="unattended",
                       action="store_true",
                       help="unattended (un)installation never prompts the user")
+    basic_group.add_option("--ca-cert-file", dest="ca_cert_file",
+                           type="string", action="callback", callback=validate_ca_cert_file_option,
+                           help="load the CA certificate from this file")
     # --on-master is used in ipa-server-install and ipa-replica-install
     # only, it isn't meant to be used on clients.
     basic_group.add_option("--on-master", dest="on_master", action="store_true",
@@ -171,6 +195,34 @@ def nickname_exists(nickname):
         else:
             return False
 
+def cert_summary(msg, cert, indent='    '):
+    if msg:
+        s = '%s\n' % msg
+    else:
+        s = ''
+    s += '%sSubject:     %s\n' % (indent, cert.subject)
+    s += '%sIssuer:      %s\n' % (indent, cert.issuer)
+    s += '%sValid From:  %s\n' % (indent, cert.valid_not_before_str)
+    s += '%sValid Until: %s\n' % (indent, cert.valid_not_after_str)
+
+    return s
+
+def get_cert_path(cert_path):
+    """
+    If a CA certificate is passed in on the command line, use that.
+
+    Else if a CA file exists in CACERT then use that.
+
+    Otherwise return None.
+    """
+    if cert_path is not None:
+        return cert_path
+
+    if os.path.exists(CACERT):
+        return CACERT
+
+    return None
+
 # Checks whether nss_ldap or nss-pam-ldapd is installed. If anyone of mandatory files was found returns True and list of all files found.
 def nssldap_exists():
     files_to_check = [{'function':'configure_ldap_conf', 'mandatory':['/etc/ldap.conf','/etc/nss_ldap.conf','/etc/libnss-ldap.conf'], 'optional':['/etc/pam_ldap.conf']},
@@ -709,7 +761,7 @@ def configure_openldap_conf(fstore, cli_basedn, cli_server):
             {'name':'empty', 'type':'empty'},
             {'name':'URI', 'type':'option', 'value':'ldaps://'+  cli_server[0]},
             {'name':'BASE', 'type':'option', 'value':cli_basedn},
-            {'name':'TLS_CACERT', 'type':'option', 'value':'/etc/ipa/ca.crt'},
+            {'name':'TLS_CACERT', 'type':'option', 'value':CACERT},
             {'name':'empty', 'type':'empty'}]
 
     target_fname = '/etc/openldap/ldap.conf'
@@ -779,7 +831,7 @@ def configure_krb5_conf(cli_realm, cli_domain, cli_server, cli_kdc, dnsok,
             kropts.append({'name':'master_kdc', 'type':'option', 'value':ipautil.format_netloc(server, 88)})
             kropts.append({'name':'admin_server', 'type':'option', 'value':ipautil.format_netloc(server, 749)})
         kropts.append({'name':'default_domain', 'type':'option', 'value':cli_domain})
-    kropts.append({'name':'pkinit_anchors', 'type':'option', 'value':'FILE:/etc/ipa/ca.crt'})
+    kropts.append({'name':'pkinit_anchors', 'type':'option', 'value':'FILE:%s' % CACERT})
     ropts = [{'name':cli_realm, 'type':'subsection', 'value':kropts}]
 
     opts.append({'name':'realms', 'type':'section', 'value':ropts})
@@ -960,7 +1012,7 @@ def configure_sssd_conf(fstore, cli_realm, cli_domain, cli_server, options, clie
     # Note that SSSD will force StartTLS because the channel is later used for
     # authentication as well if password migration is enabled. Thus set the option
     # unconditionally.
-    domain.set_option('ldap_tls_cacert', '/etc/ipa/ca.crt')
+    domain.set_option('ldap_tls_cacert', CACERT)
 
     if options.dns_updates:
         domain.set_option('ipa_dyndns_update', True)
@@ -1283,6 +1335,309 @@ def print_port_conf_info():
         "     TCP: 464\n"
         "     UDP: 464, 123 (if NTP enabled)")
 
+def get_ca_cert_from_file(url):
+    '''
+    Get the CA cert from a user supplied file and write it into the
+    CACERT file.
+
+    Raises errors.NoCertificateError if unable to read cert.
+    Raises errors.FileError if unable to write cert.
+    '''
+
+    # pylint: disable=E1101
+    try:
+        parsed = urlparse.urlparse(url, 'file')
+    except Exception, e:
+        raise errors.FileError("unable to parse file url '%s'" % (url))
+
+    if parsed.scheme != 'file':
+        raise errors.FileError("url is not a file scheme '%s'" % (url))
+
+    filename = parsed.path
+
+    if not os.path.exists(filename):
+        raise errors.FileError("file '%s' does not exist" % (filename))
+
+    if not os.path.isfile(filename):
+        raise errors.FileError("file '%s' is not a file" % (filename))
+
+    root_logger.debug("trying to retrieve CA cert from file %s", filename)
+    try:
+        cert = x509.load_certificate_from_file(filename)
+    except Exception, e:
+        raise errors.NoCertificateError(entry=filename)
+
+    try:
+        x509.write_certificate(cert.der_data, CACERT)
+    except Exception, e:
+        raise errors.FileError(reason =
+            u"cannot write certificate file '%s': %s" % (CACERT, e))
+
+def get_ca_cert_from_http(url, ca_file, warn=True):
+    '''
+    Use HTTP to retrieve the CA cert and write it into the CACERT file.
+    This is insecure and should be avoided.
+
+    Raises errors.NoCertificateError if unable to retrieve and write cert.
+    '''
+
+    if warn:
+        root_logger.warning("Downloading the CA certificate via HTTP, " +
+                            "this is INSECURE")
+
+    root_logger.debug("trying to retrieve CA cert via HTTP from %s", url)
+    try:
+
+        run(["/usr/bin/wget", "-O", ca_file, url])
+    except CalledProcessError, e:
+        raise errors.NoCertificateError(entry=url)
+
+def get_ca_cert_from_ldap(url, basedn, ca_file):
+    '''
+    Retrieve th CA cert from the LDAP server by binding to the
+    server with GSSAPI using the current Kerberos credentials.
+    Write the retrieved cert into the CACERT file.
+
+    Raises errors.NoCertificateError if cert is not found.
+    Raises errors.NetworkError if LDAP connection can't be established.
+    Raises errors.LDAPError for any other generic LDAP error.
+    Raises errors.OnlyOneValueAllowed if more than one cert is found.
+    Raises errors.FileError if unable to write cert.
+    '''
+
+    ca_cert_attr = 'cAcertificate;binary'
+    dn = DN(('cn', 'CAcert'), ('cn', 'ipa'), ('cn', 'etc'), basedn)
+
+    SASL_GSSAPI = ldap.sasl.sasl({},'GSSAPI')
+
+    root_logger.debug("trying to retrieve CA cert via LDAP from %s", url)
+
+    conn = ldap.initialize(url)
+    conn.set_option(ldap.OPT_X_SASL_NOCANON, ldap.OPT_ON)
+    try:
+        conn.sasl_interactive_bind_s('', SASL_GSSAPI)
+        result = conn.search_st(str(dn), ldap.SCOPE_BASE, 'objectclass=pkiCA',
+                                [ca_cert_attr], timeout=10)
+    except ldap.NO_SUCH_OBJECT, e:
+        root_logger.debug("get_ca_cert_from_ldap() error: %s",
+                          convert_ldap_error(e))
+        raise errors.NoCertificateError(entry=url)
+
+    except ldap.SERVER_DOWN, e:
+        root_logger.debug("get_ca_cert_from_ldap() error: %s",
+                          convert_ldap_error(e))
+        raise errors.NetworkError(uri=url, error=str(e))
+    except Exception, e:
+        root_logger.debug("get_ca_cert_from_ldap() error: %s",
+                          convert_ldap_error(e))
+        raise errors.LDAPError(str(e))
+
+    if len(result) != 1:
+        raise errors.OnlyOneValueAllowed(attr=ca_cert_attr)
+
+    attrs = result[0][1]
+    try:
+        der_cert = attrs[ca_cert_attr][0]
+    except KeyError:
+        raise errors.NoCertificateError(entry=ca_cert_attr)
+
+    try:
+        x509.write_certificate(der_cert, ca_file)
+    except Exception, e:
+        raise errors.FileError(reason =
+            u"cannot write certificate file '%s': %s" % (ca_file, e))
+
+def validate_new_ca_cert(existing_ca_cert, ca_file, ask, override=False):
+
+    try:
+        new_ca_cert = x509.load_certificate_from_file(ca_file)
+    except Exception, e:
+        raise errors.FileError(
+            "Unable to read new ca cert '%s': %s" % (ca_file, e))
+
+    if existing_ca_cert is None:
+        root_logger.info(
+            cert_summary("Successfully retrieved CA cert", new_ca_cert))
+        return
+
+    if existing_ca_cert.der_data != new_ca_cert.der_data:
+        root_logger.warning(
+            "The CA cert available from the IPA server does not match the\n"
+            "local certificate available at %s" % CACERT)
+        root_logger.warning(
+            cert_summary("Existing CA cert:", existing_ca_cert))
+        root_logger.warning(
+            cert_summary("Retrieved CA cert:", new_ca_cert))
+        if override:
+            root_logger.warning("Overriding existing CA cert\n")
+        elif not ask or not user_input(
+                "Do you want to replace the local certificate with the CA\n"
+                "certificate retrieved from the IPA server?", True):
+            raise errors.CertificateInvalidError(name='Retrieved CA')
+    else:
+        root_logger.debug(
+                "Existing CA cert and Retrieved CA cert are identical")
+        os.remove(ca_file)
+
+
+def get_ca_cert(fstore, options, server, basedn):
+    '''
+    Examine the different options and determine a method for obtaining
+    the CA cert.
+
+    If successful the CA cert will have been written into CACERT.
+
+    Raises errors.NoCertificateError if not successful.
+
+    The logic for determining how to load the CA cert is as follow:
+
+    In the OTP case (not -p and -w):
+
+    1. load from user supplied cert file
+    2. else load from HTTP
+
+    In the 'user_auth' case ((-p and -w) or interactive):
+
+    1. load from user supplied cert file
+    2. load from LDAP using SASL/GSS/Krb5 auth
+       (provides mutual authentication, integrity and security)
+    3. if LDAP failed and interactive ask for permission to
+       use insecure HTTP (default: No)
+
+    In the unattended case:
+
+    1. load from user supplied cert file
+    2. load from HTTP if --force specified else fail
+
+    In all cases if HTTP is used emit warning message
+    '''
+
+    ca_file = CACERT + ".new"
+
+    def ldap_url():
+        return urlparse.urlunparse(('ldap', ipautil.format_netloc(server),
+                                   '', '', '', ''))
+
+    def file_url():
+        return urlparse.urlunparse(('file', '', options.ca_cert_file,
+                                   '', '', ''))
+
+    def http_url():
+        return urlparse.urlunparse(('http', ipautil.format_netloc(server),
+                                   '/ipa/config/ca.crt', '', '', ''))
+
+
+    interactive = not options.unattended
+    otp_auth = options.principal is None and options.password is not None
+    existing_ca_cert = None
+
+    if options.ca_cert_file:
+        url = file_url()
+        try:
+            get_ca_cert_from_file(url)
+        except Exception, e:
+            root_logger.debug(e)
+            raise errors.NoCertificateError(entry=url)
+        root_logger.debug("CA cert provided by user, use it!")
+    else:
+        if os.path.exists(CACERT):
+            if os.path.isfile(CACERT):
+                try:
+                    existing_ca_cert = x509.load_certificate_from_file(CACERT)
+                except Exception, e:
+                    raise errors.FileError(reason=u"Unable to load existing" +
+                                           " CA cert '%s': %s" % (CACERT, e))
+            else:
+                raise errors.FileError(reason=u"Existing ca cert '%s' is " +
+                                       "not a plain file" % (CACERT))
+
+        if otp_auth:
+            if existing_ca_cert:
+                root_logger.info("OTP case, CA cert preexisted, use it")
+            else:
+                url = http_url()
+                override = not interactive
+                if interactive and not user_input(
+                    "Do you want download the CA cert from " + url + " ?\n"
+                    "(this is INSECURE)", False):
+                    raise errors.NoCertificateError(message=u"HTTP certificate"
+                            " download declined by user")
+                try:
+                    get_ca_cert_from_http(url, ca_file, override)
+                except Exception, e:
+                    root_logger.debug(e)
+                    raise errors.NoCertificateError(entry=url)
+
+                try:
+                    validate_new_ca_cert(existing_ca_cert, ca_file,
+                                         False, override)
+                except Exception, e:
+                    os.unlink(ca_file)
+                    raise
+        else:
+            # Auth with user credentials
+            url = ldap_url()
+            try:
+                get_ca_cert_from_ldap(url, basedn, ca_file)
+                try:
+                    validate_new_ca_cert(existing_ca_cert,
+                                         ca_file, interactive)
+                except Exception, e:
+                    os.unlink(ca_file)
+                    raise
+            except errors.NoCertificateError, e:
+                root_logger.debug(str(e))
+                url = http_url()
+                if existing_ca_cert:
+                    root_logger.warning(
+                        "Unable to download CA cert from LDAP\n"
+                        "but found preexisting cert, using it.\n")
+                elif interactive and not user_input(
+                    "Unable to download CA cert from LDAP.\n"
+                    "Do you want to download the CA cert from " + url + "?\n"
+                    "(this is INSECURE)", False):
+                    raise errors.NoCertificateError(message=u"HTTP "
+                                "certificate download declined by user")
+                elif not interactive and not options.force:
+                    root_logger.error(
+                        "In unattended mode without a One Time Password "
+                        "(OTP) or without --ca-cert-file\nYou must specify"
+                        " --force to retrieve the CA cert using HTTP")
+                    raise errors.NoCertificateError(message=u"HTTP "
+                                "certificate download requires --force")
+                else:
+                    try:
+                        get_ca_cert_from_http(url, ca_file)
+                    except Exception, e:
+                        root_logger.debug(e)
+                        raise errors.NoCertificateError(entry=url)
+                    try:
+                        validate_new_ca_cert(existing_ca_cert,
+                                             ca_file, interactive)
+                    except Exception, e:
+                        os.unlink(ca_file)
+                        raise
+            except Exception, e:
+                root_logger.debug(str(e))
+                raise errors.NoCertificateError(entry=url)
+
+
+        # We should have a cert now, move it to the canonical place
+        if os.path.exists(ca_file):
+            os.rename(ca_file, CACERT)
+        elif existing_ca_cert is None:
+            raise errors.InternalError(u"expected CA cert file '%s' to "
+                                       u"exist, but it's absent" % (ca_file))
+
+
+    # Make sure the file permissions are correct
+    try:
+        os.chmod(CACERT, 0644)
+    except Exception, e:
+        raise errors.FileError(reason=u"Unable set permissions on ca "
+                               u"cert '%s': %s" % (CACERT, e))
+
+
 def install(options, env, fstore, statestore):
     dnsok = False
 
@@ -1340,7 +1695,7 @@ def install(options, env, fstore, statestore):
 
     # Do discovery on the first server passed in, we'll do sanity checking
     # on any others
-    ret = ds.search(domain=options.domain, server=options.server, hostname=hostname)
+    ret = ds.search(domain=options.domain, server=options.server, hostname=hostname, ca_cert_path=get_cert_path(options.ca_cert_file))
 
     if ret == ipadiscovery.BAD_HOST_CONFIG:
         root_logger.error("Can't get the fully qualified name of this host")
@@ -1377,7 +1732,7 @@ def install(options, env, fstore, statestore):
             cli_domain_source = 'Provided interactively'
             root_logger.debug(
                 "will use interactively provided domain: %s", cli_domain)
-        ret = ds.search(domain=cli_domain, server=options.server, hostname=hostname)
+        ret = ds.search(domain=cli_domain, server=options.server, hostname=hostname, ca_cert_path=get_cert_path(options.ca_cert_file))
 
     if not cli_domain:
         if ds.domain:
@@ -1401,7 +1756,7 @@ def install(options, env, fstore, statestore):
             cli_server = [user_input("Provide your IPA server name (ex: ipa.example.com)", allow_empty = False)]
             cli_server_source = 'Provided interactively'
             root_logger.debug("will use interactively provided server: %s", cli_server[0])
-        ret = ds.search(domain=cli_domain, server=cli_server, hostname=hostname)
+        ret = ds.search(domain=cli_domain, server=cli_server, hostname=hostname, ca_cert_path=get_cert_path(options.ca_cert_file))
 
     else:
         # Only set dnsok to True if we were not passed in one or more servers
@@ -1439,6 +1794,12 @@ def install(options, env, fstore, statestore):
             "has been explicitly restricted.")
         ret = 0
 
+    if ret == ipadiscovery.NO_TLS_LDAP:
+        root_logger.warning("The LDAP server requires TLS is but we do not " +
+            "have the CA.")
+        root_logger.info("Proceeding without strict verification.")
+        ret = 0
+
     if ret != 0:
         root_logger.error("Failed to verify that %s is an IPA Server.",
             cli_server[0])
@@ -1490,7 +1851,7 @@ def install(options, env, fstore, statestore):
     # Now do a sanity check on the other servers
     if options.server and len(options.server) > 1:
         for server in options.server[1:]:
-            ret = ds.search(domain=cli_domain, server=server, hostname=hostname)
+            ret = ds.search(domain=cli_domain, server=server, hostname=hostname, ca_cert_path=get_cert_path(options.ca_cert_file))
             if ret == ipadiscovery.NOT_IPA_SERVER:
                 root_logger.error("%s is not an IPA v2 Server.", server)
                 print_port_conf_info()
@@ -1539,21 +1900,6 @@ def install(options, env, fstore, statestore):
             root_logger.debug(
                 "will use principal provided as option: %s", options.principal)
 
-    # Get the CA certificate
-    try:
-        # Remove anything already there so that wget doesn't use its
-        # too-clever renaming feature
-        os.remove("/etc/ipa/ca.crt")
-    except Exception:
-        pass
-
-    try:
-        run(["/usr/bin/wget", "-O", "/etc/ipa/ca.crt", "http://%s/ipa/config/ca.crt" % ipautil.format_netloc(cli_server[0])])
-    except CalledProcessError, e:
-        root_logger.error(
-            'Retrieving CA from %s failed: %s', cli_server[0], str(e))
-        return CLIENT_INSTALL_ERROR
-
     if not options.on_master:
         nolog = tuple()
         # First test out the kerberos configuration
@@ -1650,6 +1996,15 @@ def install(options, env, fstore, statestore):
                 join_args.append(password)
                 nolog = (password,)
 
+            # Get the CA certificate
+            try:
+                os.environ['KRB5_CONFIG'] = env['KRB5_CONFIG']
+                get_ca_cert(fstore, options, cli_server[0], cli_basedn)
+                del os.environ['KRB5_CONFIG']
+            except Exception, e:
+                root_logger.error("Cannot obtain CA certificate\n%s", e)
+                return CLIENT_INSTALL_ERROR
+
             # Now join the domain
             (stdout, stderr, returncode) = run(join_args, raiseonerr=False, env=env, nolog=nolog)
 
@@ -1717,7 +2072,7 @@ def install(options, env, fstore, statestore):
 
     # Add the CA to the default NSS database and trust it
     try:
-        run(["/usr/bin/certutil", "-A", "-d", "/etc/pki/nssdb", "-n", "IPA CA", "-t", "CT,C,C", "-a", "-i", "/etc/ipa/ca.crt"])
+        run(["/usr/bin/certutil", "-A", "-d", "/etc/pki/nssdb", "-n", "IPA CA", "-t", "CT,C,C", "-a", "-i", CACERT])
     except CalledProcessError, e:
         root_logger.info("Failed to add CA to the default NSS database.")
         return CLIENT_INSTALL_ERROR
diff --git a/ipa-client/ipaclient/ipadiscovery.py b/ipa-client/ipaclient/ipadiscovery.py
index 2214a81..18b77a6 100644
--- a/ipa-client/ipaclient/ipadiscovery.py
+++ b/ipa-client/ipaclient/ipadiscovery.py
@@ -30,11 +30,14 @@ from ipapython.ipautil import run, CalledProcessError, valid_ip, get_ipa_basedn,
                               realm_to_suffix, format_netloc
 from ipapython.dn import DN
 
+CACERT = '/etc/ipa/ca.crt'
+
 NOT_FQDN = -1
 NO_LDAP_SERVER = -2
 REALM_NOT_FOUND = -3
 NOT_IPA_SERVER = -4
 NO_ACCESS_TO_LDAP = -5
+NO_TLS_LDAP = -6
 BAD_HOST_CONFIG = -10
 UNKNOWN_ERROR = -15
 
@@ -45,6 +48,7 @@ error_names = {
     REALM_NOT_FOUND: 'REALM_NOT_FOUND',
     NOT_IPA_SERVER: 'NOT_IPA_SERVER',
     NO_ACCESS_TO_LDAP: 'NO_ACCESS_TO_LDAP',
+    NO_TLS_LDAP: 'NO_TLS_LDAP',
     BAD_HOST_CONFIG: 'BAD_HOST_CONFIG',
     UNKNOWN_ERROR: 'UNKNOWN_ERROR',
 }
@@ -135,7 +139,7 @@ class IPADiscovery(object):
                 domain = domain[p+1:]
         return (None, None)
 
-    def search(self, domain = "", server = "", hostname=None):
+    def search(self, domain = "", server = "", hostname=None, ca_cert_path=None):
         root_logger.debug("[IPA Discovery]")
         root_logger.debug(
             'Starting IPA discovery with domain=%s, server=%s, hostname=%s',
@@ -224,14 +228,14 @@ class IPADiscovery(object):
         ldapaccess = True
         if self.server:
             # check ldap now
-            ldapret = self.ipacheckldap(self.server, self.realm)
+            ldapret = self.ipacheckldap(self.server, self.realm, ca_cert_path=ca_cert_path)
 
             if ldapret[0] == 0:
                 self.server = ldapret[1]
                 self.realm = ldapret[2]
                 self.server_source = self.realm_source = (
                     'Discovered from LDAP DNS records in %s' % self.server)
-            elif ldapret[0] == NO_ACCESS_TO_LDAP:
+            elif ldapret[0] == NO_ACCESS_TO_LDAP or ldapret[0] == NO_TLS_LDAP:
                 ldapaccess = False
 
         # If one of LDAP servers checked rejects access (maybe anonymous
@@ -260,12 +264,10 @@ class IPADiscovery(object):
 
         return ldapret[0]
 
-    def ipacheckldap(self, thost, trealm):
+    def ipacheckldap(self, thost, trealm, ca_cert_path=None):
         """
         Given a host and kerberos realm verify that it is an IPA LDAP
-        server hosting the realm. The connection is an SSL connection
-        so the remote IPA CA cert must be available at
-        http://HOST/ipa/config/ca.crt
+        server hosting the realm.
 
         Returns a list [errno, host, realm] or an empty list on error.
         Errno is an error number:
@@ -279,31 +281,17 @@ class IPADiscovery(object):
 
         i = 0
 
-        # Get the CA certificate
-        try:
-            # Create TempDir
-            temp_ca_dir = tempfile.mkdtemp()
-        except OSError, e:
-            raise RuntimeError("Creating temporary directory failed: %s" % str(e))
-
-        try:
-            run(["/usr/bin/wget", "-O", "%s/ca.crt" % temp_ca_dir, "-T", "15", "-t", "2",
-                 "http://%s/ipa/config/ca.crt" % format_netloc(thost)])
-        except CalledProcessError, e:
-            root_logger.error('Retrieving CA from %s failed', thost)
-            root_logger.debug('Retrieving CA from %s failed: %s', thost, str(e))
-            return [NOT_IPA_SERVER]
-
         #now verify the server is really an IPA server
         try:
             ldap_url = "ldap://" + format_netloc(thost, 389)
             root_logger.debug("Init LDAP connection with: %s", ldap_url)
             lh = ldap.initialize(ldap_url)
-            ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, True)
-            ldap.set_option(ldap.OPT_X_TLS_CACERTFILE, "%s/ca.crt" % temp_ca_dir)
+            if ca_cert_path:
+                ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, True)
+                ldap.set_option(ldap.OPT_X_TLS_CACERTFILE, ca_cert_path)
+                lh.set_option(ldap.OPT_X_TLS_DEMAND, True)
+                lh.start_tls_s()
             lh.set_option(ldap.OPT_PROTOCOL_VERSION, 3)
-            lh.set_option(ldap.OPT_X_TLS_DEMAND, True)
-            lh.start_tls_s()
             lh.simple_bind_s("","")
 
             # get IPA base DN
@@ -358,14 +346,16 @@ class IPADiscovery(object):
                 root_logger.debug("LDAP Error: Anonymous acces not allowed")
                 return [NO_ACCESS_TO_LDAP]
 
+            # We should only get UNWILLING_TO_PERFORM if the remote LDAP server
+            # has minssf > 0 and we have attempted a non-TLS connection.
+            if ca_cert_path is None and isinstance(err, ldap.UNWILLING_TO_PERFORM):
+                root_logger.debug("LDAP server returned UNWILLING_TO_PERFORM. This likely means that minssf is enabled")
+                return [NO_TLS_LDAP]
+



More information about the Pkg-freeipa-devel mailing list