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