def read_cert_cn(given_cert):
    '''Given any cert object and passphrase, return its subject CN or None.'''
    common.logging_info("Getting CN for given certificate.")
    try:
        return str(given_cert.get_subject().CN)
    except crypto.Error:
        common.logging_error("Could not get the certificate's common name.")
        return None
Esempio n. 2
0
def join_group_manifest(given_computer_manifest_name,
                        given_group_manifest_name):
    '''Modifes a computer manifest's included_manifests key to include the name of a group manifest,
        effectively making the computer a member of the group and its parent groups.
        Both manifests must be present and valid plists.  Returns true if successful, false otherwise.'''
    # Filesystem paths for manifests:
    given_computer_manifest_name = given_computer_manifest_name.upper(
    )  # if not already
    computer_manifest_path = os.path.join(munki_repo.COMPUTER_MANIFESTS_PATH,
                                          given_computer_manifest_name)
    group_manifest_path = os.path.join(munki_repo.GROUP_MANIFESTS_PATH,
                                       given_group_manifest_name)

    # Catch missing manifests:
    if not os.path.exists(computer_manifest_path):
        common.logging_error("Computer manifest not found at %s." %
                             computer_manifest_path)
        return False
    if not os.path.exists(group_manifest_path):
        common.logging_error("Group manifest not found at %s." %
                             group_manifest_path)
        return False

    # Load manifests:
    try:
        computer_manifest_dict = plistlib.readPlist(computer_manifest_path)
    except xml.parsers.expat.ExpatError:
        common.logging_error("Computer manifest %s is invalid." %
                             given_computer_manifest_name)
        return False
    try:
        group_manifest_dict = plistlib.readPlist(group_manifest_path)
    except xml.parsers.expat.ExpatError:
        common.logging_error("Group manifest %s is invalid." %
                             given_group_manifest_name)
        return False

    # Modify computer manifest and save it:
    common.logging_info(
        "Adding %(computer)s to %(group)s." % {
            'computer': given_computer_manifest_name,
            'group': given_group_manifest_name
        })
    # Make sure these stay empty; data should come from included (group) manifest only:
    computer_manifest_dict['managed_installs'] = []
    computer_manifest_dict['managed_uninstalls'] = []
    computer_manifest_dict['catalogs'] = munki_repo.CATALOG_ARRAY
    # Add to group:
    computer_manifest_dict['included_manifests'] = []
    computer_manifest_dict['included_manifests'].append(
        'groups/%s' % given_group_manifest_name)
    try:
        plistlib.writePlist(computer_manifest_dict, computer_manifest_path)
        return True
    except TypeError:
        common.logging_error("Failed to write manifest for %s." %
                             given_computer_manifest_name)
        return False
def make_p12_with_ca_cert(given_ca_cert, given_passphrase):
    '''Given CA cert object and passphrase, create a PKCS container
        and return it as data.  Return None if something went wrong.'''
    common.logging_info("Generating PKCS12 store for given CA certificate.")
    p = crypto.PKCS12()
    try:
        p.set_ca_certificates([given_ca_cert])
    except crypto.Error:
        common.logging_error(
            "Could create a p12 object with given CA certificate.")
        return None
    try:
        p12_ca_data = p.export(passphrase=given_passphrase)
    except crypto.Error:
        common.logging_error("Could not export data from p12 object.")
        return None
    return p12_ca_data
def verify_signed_message(given_message, given_encoded_sig, given_cert):
    '''Given strings for the message, its signature, and a certificate object,
        verify the message.  Returns true or false.'''
    # Decode given signature (expected base64 encoding):
    try:
        decoded_sig = base64.b64decode(given_encoded_sig)
    except:
        common.logging_error("Could not interpret encoded signature.")
        return False
    # Verify the signature:
    try:
        # OpenSSL is funny.  A successful crypto.verify() returns a None object!
        crypto.verify(given_cert, decoded_sig, given_message,
                      config_client_pki.CSR_SIGNING_HASH_ALGORITHM)
        return True
    except crypto.Error:
        common.logging_error("Message fails verification.")
        return False
def do_enrollment(given_serial):
    '''Abstracted method covering the enrollment procedure.
        Returns tar file contents or None.'''
    # Validate serial:
    given_serial = devices.validate_serial_number(given_serial)
    if not given_serial:
        common.logging_error("Serial number failed validation.")
        return None
    # Generate manifest:
    manifests.make_computer_manifest(given_serial)
    # Create private key:
    client_key = security.generate_private_key()
    # Create CSR:
    client_csr = security.generate_csr(client_key, given_serial)
    # Read CA certificate:
    ca_cert = security.read_ca_cert()
    ca_cert_p12_data = security.make_p12_with_ca_cert(ca_cert, given_serial)
    ca_cert_cn = security.read_cert_cn(ca_cert)
    # Sign CSR:
    client_cert = security.sign_with_ca(client_csr, ca_cert)
    # Generate mobileconfig:
    mobileconfig_contents = make_mobileconfig(given_serial, ca_cert_p12_data,
                                              ca_cert_cn)
    if not mobileconfig_contents:
        common.logging_error("Failed to generate mobileconfig_contents.")
        return None
    # Generate identity PEM (client key + client cert + CA cert):
    pki_contents = security.make_identity_pem_string(client_key, client_cert,
                                                     ca_cert)
    if not pki_contents:
        common.logging_error("Failed to generate pki_contents.")
        return None
    # Return tar file:
    return make_tar_file(given_serial, pki_contents, mobileconfig_contents)
def make_identity_pem_string(given_key, given_cert, given_ca_cert):
    '''Given objects for client key, client cert, and CA cert,
        extract their contents in PEM format and combine them
        into a single text string.  Return that string or None.'''
    common.logging_info("Generating client identity PEM.")
    try:
        key_pem = crypto.dump_privatekey(crypto.FILETYPE_PEM, given_key)
    except crypto.Error:
        common.logging_error("Could not get PEM contents of private key.")
        return None
    try:
        cert_pem = crypto.dump_certificate(crypto.FILETYPE_PEM, given_cert)
    except crypto.Error:
        common.logging_error(
            "Could not get PEM contents of client certificate.")
        return None
    try:
        ca_pem = crypto.dump_certificate(crypto.FILETYPE_PEM, given_ca_cert)
    except crypto.Error:
        common.logging_error("Could not get PEM contents of CA certificate.")
        return None
    combined_pem = '%(key_pem)s\n%(cert_pem)s\n%(ca_pem)s' % {
        'key_pem': key_pem,
        'cert_pem': cert_pem,
        'ca_pem': ca_pem
    }
    return combined_pem
def read_ca_cert():
    '''Loads CA certificate from the filesystem and returns it as an object.
        Returns None if something went wrong.'''
    common.logging_info("Loading CA certificate.")
    # Check for missing CA certificate:
    if not os.path.exists(config_ca.CA_CERT_FILE_PATH):
        common.logging_error("No CA certificate file found at %s." %
                             config_ca.CA_CERT_FILE_PATH)
        return None
    # Read and load CA certificate:
    try:
        file_object = open(config_ca.CA_CERT_FILE_PATH, 'r')
        file_contents = file_object.read()
        file_object.close()
    except IOError:
        return None
    # Return CA cert:
    try:
        return crypto.load_certificate(crypto.FILETYPE_PEM, file_contents)
    except crypto.Error:
        common.logging_error("Could not read CA certificate from %s." %
                             config_ca.CA_CERT_FILE_PATH)
        return None
Esempio n. 8
0
def make_computer_manifest(given_serial):
    '''Checks for the presence and validity of an existing manifest for this client in the repository.
        Creates a new manifest if an existing one is not found or is invalid.'''
    # Filesystem path for this computer's manifest:
    computer_manifest_name = given_serial.upper()  # if not already
    computer_manifest_path = os.path.join(munki_repo.COMPUTER_MANIFESTS_PATH,
                                          computer_manifest_name)
    # Control variable: should a new manifest be created?
    # Assume yes, unless an existing manifest is found and is valid.
    should_create_new_client_manifest = True

    # Catch missing computer manifests directory:
    if not os.path.exists(munki_repo.COMPUTER_MANIFESTS_PATH):
        common.logging_error("Computers manifests directory not found at %s." %
                             munki_repo.COMPUTER_MANIFESTS_PATH)
        raise

    # Check existing manifest for this client:
    if os.path.exists(computer_manifest_path):
        common.logging_info(
            "Manifest for %s already in repository. Checking it." %
            computer_manifest_name)
        try:
            computer_manifest_dict = plistlib.readPlist(computer_manifest_path)
            # Manifest already exists; do not overwrite if it's a valid dict!
            if computer_manifest_dict:
                should_create_new_client_manifest = False
                common.logging_info("Manifest for %s should be left alone." %
                                    computer_manifest_name)
        except xml.parsers.expat.ExpatError:
            common.logging_error("Manifest for %s is invalid. Will recreate." %
                                 computer_manifest_name)

    # Build a new client manifest if required:
    if should_create_new_client_manifest:
        common.logging_info("Creating new manifest for %s." %
                            computer_manifest_name)
        computer_manifest_dict = {}
        computer_manifest_dict['managed_installs'] = []
        computer_manifest_dict['managed_uninstalls'] = []
        computer_manifest_dict['catalogs'] = munki_repo.CATALOG_ARRAY
        computer_manifest_dict['included_manifests'] = [
            'groups/%s' % munki_repo.DEFAULT_GROUP
        ]
        try:
            plistlib.writePlist(computer_manifest_dict, computer_manifest_path)
        except TypeError:
            common.logging_error("Failed to write manifest for %s." %
                                 computer_manifest_name)
Esempio n. 9
0
def write_metadata_to_manifest(given_computer_manifest_name,
                               given_metadata_key, given_metadata_value):
    '''Modifes the given computer manifest's _metadata dict, adding (overwriting) the given key and value.
        Given manifest must be present and a valid plist.  Returns true if successful, false otherwise.'''
    # Filesystem paths for the manifest:
    given_computer_manifest_name = given_computer_manifest_name.upper(
    )  # if not already
    computer_manifest_path = os.path.join(munki_repo.COMPUTER_MANIFESTS_PATH,
                                          given_computer_manifest_name)

    # Catch missing manifest:
    if not os.path.exists(computer_manifest_path):
        common.logging_error("Computer manifest not found at %s." %
                             computer_manifest_path)
        return False

    # Load manifest:
    try:
        computer_manifest_dict = plistlib.readPlist(computer_manifest_path)
    except xml.parsers.expat.ExpatError:
        common.logging_error("Computer manifest %s is invalid." %
                             given_computer_manifest_name)
        return False

    # Load manifest metadata or start with blank:
    try:
        manifest_metadata_dict = computer_manifest_dict['_metadata']
    except KeyError:
        manifest_metadata_dict = {}

    # Modify metadata dict:
    common.logging_info("Adding %(key)s to %(manifest)s." % {
        'key': given_metadata_key,
        'manifest': given_computer_manifest_name
    })
    manifest_metadata_dict[given_metadata_key] = given_metadata_value
    computer_manifest_dict['_metadata'] = manifest_metadata_dict

    # Save manifest:
    try:
        plistlib.writePlist(computer_manifest_dict, computer_manifest_path)
        return True
    except TypeError:
        common.logging_error("Failed to write manifest for %s." %
                             given_computer_manifest_name)
        return False
Esempio n. 10
0
def list_group_manifests():
    '''Scans the munki repository for manifests that we designate as computer groups.
        Returns an array of dictionaries, each dict representing a group.'''
    # Catch missing manifests path:
    if not os.path.exists(munki_repo.GROUP_MANIFESTS_PATH):
        common.logging_error("Missing group manifests directory.")
        return False, []  # empty list
    path_glob = '%s/*' % str(munki_repo.GROUP_MANIFESTS_PATH)
    common.logging_info("Listing group manifests: %s" % path_glob)
    try:
        search_results_path_array = glob.glob(path_glob)
    except:
        common.logging_error(
            "Glob error while listing group manifests directory.")
        return False, []  # empty list
    # Go through the returned group manifests, validating them and reading metadata:
    groups_array = []
    for manifest_path in search_results_path_array:
        try:
            manifest_dict = plistlib.readPlist(manifest_path)
            have_valid_manifest = True
        except xml.parsers.expat.ExpatError:
            # Skip manifests that cannot be parsed as plists:
            common.logging_error("Skipping invalid group manifest: %s" %
                                 manifest_path)
            have_valid_manifest = False
            pass
        if have_valid_manifest:
            # Build dictionary representing this group:
            group_details_dict = {}
            # At least the name is present:
            group_details_dict['name'] = os.path.basename(manifest_path)
            # Assign defaults unless we can override them with metadata:
            group_details_dict['display_name'] = group_details_dict['name']
            group_details_dict['description'] = group_details_dict['name']
            group_details_dict[
                'computer_name_prefix'] = munki_repo.DEFAULT_COMPUTER_NAME_PREFIX
            # Check for metadata in the manifest:
            try:
                manifest_metadata_dict = manifest_dict['_metadata']
                have_manifest_metadata = True
            except KeyError:
                have_manifest_metadata = False
                pass
            # Attempt to read various metadata keys:
            if have_manifest_metadata:
                try:
                    group_details_dict[
                        'display_name'] = manifest_metadata_dict[
                            'display_name']
                except KeyError:
                    pass
                try:
                    group_details_dict['description'] = manifest_metadata_dict[
                        'description']
                except KeyError:
                    pass
                try:
                    group_details_dict[
                        'computer_name_prefix'] = manifest_metadata_dict[
                            'computer_name_prefix']
                except KeyError:
                    pass
            # Append group details dict to ths groups array:
            groups_array.append(group_details_dict)
    # Catch empty groups_array:
    if len(groups_array) == 0:
        return False, []  # empty list
    # Return group list:
    return True, groups_array
def make_tar_file(given_serial, given_pki_contents,
                  given_mobileconfig_contents):
    '''Creates a tarfile with the configuration profile and client identity.
        Returns base64-encoded data representing the tarfile contents or
        None if something went wrong.'''
    common.logging_info("Generating tar file response for %s." % given_serial)

    tar_file_name = "mes-%s.tar" % given_serial.upper()
    tar_file_path = os.path.join(config_app.TEMP_DIR, tar_file_name)

    # Remove existing archive:
    if os.path.exists(tar_file_path):
        os.remove(tar_file_path)
        common.logging_info("Removed existing temp file: %s." % tar_file_path)

    # Create file objects:
    member_files_array = []
    # Config profile:
    member_file_meta_dict = {}
    member_file_meta_dict['file_name'] = "client-enrollment.mobileconfig"
    member_file_meta_dict['file_size'] = len(given_mobileconfig_contents)
    member_file_meta_dict['file_object'] = StringIO(
        given_mobileconfig_contents)
    member_files_array.append(member_file_meta_dict)

    # Identity:
    member_file_meta_dict = {}
    member_file_meta_dict['file_name'] = "client-identity.pem"
    member_file_meta_dict['file_size'] = len(given_pki_contents)
    member_file_meta_dict['file_object'] = StringIO(given_pki_contents)
    member_files_array.append(member_file_meta_dict)

    # Create new archive:
    try:
        tar_file = tarfile.open(tar_file_path, 'w')
    except tarfile.TarError:
        common.logging_error("Failed to create archive file at %s." %
                             tar_file_path)
        return None

    # Add files to tar:
    for m in member_files_array:
        try:
            file_info = tarfile.TarInfo(m['file_name'])
            file_info.size = m['file_size']
            if file_info.size == 0:
                common.logging_error("%s appears to be empty." %
                                     m['file_name'])
            tar_file.addfile(file_info, m['file_object'])
        except:
            common.logging_error("Failed to add %s to archive." %
                                 m['file_name'])

    tar_file.close()

    # Read archive:
    try:
        tar_file = open(tar_file_path, 'r')
        attachment_contents = base64.b64encode(tar_file.read())
        tar_file.close()
    except tarfile.TarError:
        common.logging_error("Failed to read newly created archive at %s." %
                             tar_file_path)
        return None

    # Remove archive:
    if os.path.exists(tar_file_path):
        os.remove(tar_file_path)
        common.logging_info("Removed archive: %s." % tar_file_path)

    # Return:
    return attachment_contents
def process_request():
    '''Method called when a request is sent to this web app.'''
    # Defaults:
    # Default response type is plist unless set otherwise:
    response_is_tar_file = False
    response_attachment = None
    response_dict = {}
    response = None
    # Get HTTP POST variables:
    # All interactions must have a command POST variables.
    try:
        command = request.form['command']
    except NameError:
        common.logging_error("No command sent in POST.")
        return None
    except KeyError:
        common.logging_error("No command sent in POST.")
        return None
    # Toss unrecognized commands:
    if command not in config_server_app.TRANSACTIONS:
        return None

    # Handle authentication if required:
    if command not in config_server_app.ANON_TRANSACTIONS:
        # Grab additional POST variables:
        try:
            message = request.form['message']
        except KeyError:
            common.logging_error("Message not sent in POST.")
            return None
        try:
            signature = request.form['signature']
        except KeyError:
            common.logging_error("Signature not sent in POST.")
            return None
        try:
            cert_pem_str = request.form['certificate']
        except KeyError:
            common.logging_error("Certificate not sent in POST.")
            return None
        # Load certificate:
        client_cert = security.pem_to_cert(cert_pem_str)
        if not client_cert:
            common.logging_error("Certificate data is invalid!")
        # Determine computer's manifest name by reading the CN of its certificate.
        computer_manifest_name = security.read_cert_cn(client_cert).upper()
        if not computer_manifest_name:
            return None
        # Authentication: Verify signed message using certificate's public key.
        if not security.verify_signed_message(message, signature, client_cert):
            common.logging_error(
                "Authentication error: failed to verify the signed message.")
            return None

    # Handle command:
    if command == 'request-enrollment':
        client_serial = request.form['message']
        common.logging_info("Processing enrollment request for %s..." %
                            client_serial)
        response_attachment = enrollment.do_enrollment(client_serial)
        if not response_attachment:
            common.logging_error("Invalid tar file returned by do_enrollment!")
            return None
        response_is_tar_file = True
    elif command == 'transaction-a':
        common.logging_info("Processing transaction A...")
        response_dict[
            'computer_manifest'] = manifests.get_computer_manifest_details(
                computer_manifest_name)
        ignored, response_dict[
            'group_manifests_array'] = manifests.list_group_manifests()
    elif command == 'transaction-b':
        common.logging_info("Processing transaction B...")
        response_dict['joined_group'] = False
        response_dict['recorded_name'] = False
        try:
            message_dict = plistlib.readPlistFromString(message)
        except xml.parsers.expat.ExpatError:
            message_dict = {}
        try:
            group_manifest_name = message_dict['group_manifest_name']
            response_dict['joined_group'] = manifests.join_group_manifest(
                computer_manifest_name, group_manifest_name)
        except KeyError:
            pass
        try:
            desired_computer_name = message_dict['desired_computer_name']
            response_dict[
                'recorded_name'] = manifests.write_metadata_to_manifest(
                    computer_manifest_name, 'computer_name',
                    desired_computer_name)
        except KeyError:
            pass
        common.logging_info("Completed transaction B.")
    elif command == 'join-manifest':  # DEPRECATED!
        common.logging_info("Processing request to join a group manifest...")
        group_manifest_name = message
        response_dict['result'] = manifests.join_group_manifest(
            computer_manifest_name, group_manifest_name)
        common.logging_info("Completed request to join a group manifest.")
    elif command == 'set-name':  # DEPRECATED!
        common.logging_info("Processing request to store computer name...")
        desired_computer_name = message
        response_dict['result'] = manifests.write_metadata_to_manifest(
            computer_manifest_name, 'computer_name', desired_computer_name)
        common.logging_info("Completed request to store computer name.")

    # Process responses:
    if response_is_tar_file and response_attachment:
        common.logging_info("Processing response: tar file.")
        response = make_response(response_attachment)
        response.headers['Content-Disposition'] = "attachment"
    elif response_dict:
        common.logging_info("Processing response: plist.")
        try:
            response_plist_str = plistlib.writePlistToString(response_dict)
        except xml.parsers.expat.ExpatError:
            response_plist_str = ''
        except TypeError:
            response_plist_str = ''
        if response_plist_str:
            response = make_response(response_plist_str)
    # Return response:
    if not response:
        return None
    return response
        response = make_response(response_attachment)
        response.headers['Content-Disposition'] = "attachment"
    elif response_dict:
        common.logging_info("Processing response: plist.")
        try:
            response_plist_str = plistlib.writePlistToString(response_dict)
        except xml.parsers.expat.ExpatError:
            response_plist_str = ''
        except TypeError:
            response_plist_str = ''
        if response_plist_str:
            response = make_response(response_plist_str)
    # Return response:
    if not response:
        return None
    return response


# Launch:
if __name__ == "__main__":
    if config_server_app.DEBUG_MODE:
        print "WARNING: Web app is running in debug mode!"
        app.run(host="0.0.0.0", port=config_server_app.PORT, debug=True)
    else:
        try:
            app.run(host="127.0.0.1", port=config_server_app.PORT, debug=False)
        except:
            common.logging_error(
                "Generic error.  The enrollment server could not complete the request."
            )
def sign_with_ca(given_csr, given_ca_cert):
    '''Signs a CSR with the specified CA's private key.
        Returns the signed certificate or None.'''
    common.logging_info("Signing certificate signing request.")
    # Check CSR:
    if not given_csr:
        common.logging_error("Invalid CSR.")
        return None
    # Check CA certificate:
    if not given_ca_cert:
        common.logging_error("Cannot sign CSR - CA certificate error.")
        return None
    # Check for missing CA key file:
    if not os.path.exists(config_ca.CA_PRIVATE_KEY_FILE_PATH):
        common.logging_error("No CA private key file found at %s." %
                             config_ca.CA_PRIVATE_KEY_FILE_PATH)
        return None
    # Read and load CA private key:
    try:
        file_object = open(config_ca.CA_PRIVATE_KEY_FILE_PATH, 'r')
        file_contents = file_object.read()
        file_object.close()
    except IOError:
        return None
    try:
        ca_key = crypto.load_privatekey(crypto.FILETYPE_PEM, file_contents,
                                        config_ca.CA_PRIVATE_KEY_PASSPHRASE)
    except crypto.Error:
        common.logging_error("Could not read CA private key from %s." %
                             config_ca.CA_PRIVATE_KEY_FILE_PATH)
        return None
    # Set issuer:
    try:
        given_csr.set_issuer(given_ca_cert.get_subject())
    except crypto.Error:
        common.logging_error("Could not set issuer for CSR to CA subject.")
        ca_key = None  # safety!
        return None
    # Sign the CSR with the CA's private key:
    try:
        given_csr.sign(ca_key, config_client_pki.CSR_SIGNING_HASH_ALGORITHM)
    except crypto.Error:
        common.logging_error("Could not sign CSR!")
        ca_key = None  # safety!
        return None
    # Wipe CA private key and these variables before exiting this method:
    ca_key = None
    file_contents = None
    file_object = None
    # Return signed cert:
    return given_csr