Skip to content

Instantly share code, notes, and snippets.

@pudquick
Last active September 30, 2016 14:43
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save pudquick/7704254 to your computer and use it in GitHub Desktop.
Save pudquick/7704254 to your computer and use it in GitHub Desktop.
Security automation for ca.pem + client.pem
"""
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)
@gregneagle
Copy link

Any tips/walkthrough on building a site for testing?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment