Last active
September 30, 2016 14:43
-
-
Save pudquick/7704254 to your computer and use it in GitHub Desktop.
Security automation for ca.pem + client.pem
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
First attempt at bringing security into play with the .pem files. | |
Only the security tool is used, no additional tools (openssl, etc.). | |
This code does the following: | |
- Creates the specified keychain if it doesn't exist | |
- Unlocks it with the specified password | |
- Configures it to not lock | |
- Adds it to the keychain search paths if it's not present already (necessary for 10.9) | |
- Import the client.pem cert / identity | |
- Import the CA.pem cert as a trusted cert (but only at the user level, stored in this specific keychain) | |
- Checks for an existing identity preference for the site: | |
- If one is found: | |
- If it's wrong, it's corrected | |
- If it's correct, it's left alone | |
- If one is not found, it's created | |
The end result should be: | |
- 'munki.keychain' created (if necessary) and unlocked | |
- CA from ca.pem trusted | |
- Client cert from client.pem imported | |
- Identity preference for specific site created and configured with client cert | |
This should work from OS X 10.5.4 - 10.9+ | |
""" | |
import os.path, subprocess, re | |
def pref(something): | |
return None | |
def security(verb_name, *args): | |
cmd = ['/usr/bin/security', verb_name] + list(args) | |
proc = subprocess.Popen(cmd, shell=False, bufsize=1, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) | |
(output, err) = proc.communicate() | |
return (proc.returncode, output, err) | |
if True: | |
site_addr = 'https://trustedsite.dev/' | |
client_cert = '/Users/mike/Desktop/certs/client.pem' | |
ca_cert = '/Users/mike/Desktop/certs/ca.pem' | |
# Defined information for the keychain | |
default_chain = 'munki.keychain' | |
default_pass = 'munki' | |
keychain_name = (pref('keychainName') or default_chain).strip() | |
keychain_pass = (pref('keychainPass') or default_pass).strip() | |
# If we have an odd path that appears to be all directory and no file name, revert to default filename | |
if not os.path.basename(keychain_name): | |
keychain_name = default_chain | |
# Check to make sure it's just a simple file name, no directory information | |
if os.path.dirname(keychain_name): | |
# All keychains should be a relative filename, so we'll drop down to the base name | |
keychain_name = os.path.basename(keychain_name).strip() or default_chain | |
# Correct the filename to include '.keychain' if not already present | |
if not keychain_name.lower().endswith('.keychain'): | |
keychain_name += '.keychain' | |
# Check to see if the file already exists at ~/Library/Keychains/name | |
abs_keychain_name = os.path.realpath(os.path.join(os.path.expanduser('~/Library/Keychains'), keychain_name)) | |
if not os.path.exists(abs_keychain_name): | |
# Keychain doesn't appear to be present, create it | |
(code, output, err) = security('create-keychain', '-p', keychain_pass, keychain_name) | |
# Check to make sure the keychain is in the search path | |
(code, output, err) = security('list-keychains', '-d', 'user') | |
# Split the output and strip it of whitespace and leading/trailing quotes, the result are absolute paths to keychains | |
# Preserve the order in case we need to append to them | |
search_keychains = [x.strip().strip('"') for x in output.split('\n') if x.strip()] | |
if not abs_keychain_name in search_keychains: | |
# Keychain is not in the search paths | |
search_keychains.append(abs_keychain_name) | |
(code, output, err) = security('list-keychains', '-d', 'user', '-s', *search_keychains) | |
# Configure the keychain as unlocked and non-locking | |
(code, output, err) = security('unlock-keychain', '-p', keychain_pass, keychain_name) | |
(code, output, err) = security('set-keychain-settings', keychain_name) | |
# Just blanket import the client cert - if it's already there, it'll return a warning we'll ignore | |
if os.path.exists(client_cert): | |
(code, output, err) = security('import', client_cert, '-k', keychain_name) | |
# Same goes for the CA cert | |
if os.path.exists(ca_cert): | |
(code, output, err) = security('add-trusted-cert', '-k', keychain_name, ca_cert) | |
# Set up an identity if it doesn't exist already for our site | |
# First we need to find the existing identity in our keychain | |
(code, output, err) = security('find-identity', keychain_name) | |
if ' 1 identities found' in output: | |
# We have a solitary match and can configure / verify the identity preference | |
id_hash = re.findall(r'\W+1\)\W+([0-9A-F]+)\W', output)[0] | |
# First, check to see if we have an identity already | |
(code, output, err) = security('get-identity-preference', '-s', site_addr, '-Z') | |
create_identity = False | |
if code == 0: | |
# No error, we found an identity | |
# Check if it matches the one we want | |
current_hash = re.match(r'SHA-1 hash:\W+([A-F0-9]+)\W', output).group(1) | |
if id_hash != current_hash: | |
# We only care if there's a different hash being used. | |
# Remove the incorrect one. | |
(code, output, err) = security('set-identity-preference', '-n', '-s', site_addr) | |
# Signal that we want to create a new identity preference | |
create_identity = True | |
elif id_hash not in output: | |
# Non-zero error code and hash not detected in output | |
# Signal that we want to create a new identity preference | |
create_identity = True | |
if create_identity: | |
# This code was moved into a common block that both routes could access as it's a little complicated. | |
# security will only create an identity preference in the default keychain - which means a default has to be defined/selected | |
# For normal users, this is login.keychain - but for root there's no login.keychain and no default keychain configured | |
# So we'll handle the case of no default keychain (just set one) as well as pre-existing default keychain | |
# (in which case we set it long enough to create the preference, then set it back) | |
(code, output, err) = security('default-keychain', '-d', 'user') | |
if code == 0: | |
# One is defined, remember the path | |
default_keychain = [x.strip().strip('"') for x in output.split('\n') if x.strip()][0] | |
else: | |
default_keychain = None | |
# Temporarily assign the default keychain to ours | |
(code, output, err) = security('default-keychain', '-d', 'user', '-s', abs_keychain_name) | |
# Create the identity preference | |
(code, output, err) = security('set-identity-preference', '-s', site_addr, '-Z', id_hash, keychain_name) | |
if default_keychain: | |
# We originally had a different one, set it back | |
(code, output, err) = security('default-keychain', '-d', 'user', '-s', default_keychain) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Any tips/walkthrough on building a site for testing?