tools: add tool for iOS mobileconfig conversion

This tool will convert an iOS 'mobileconfig' file into the IWD
format. The tool only supports PEAP and TTLS networks, including
hotspots.

It will also parse out any certificate chains found in the
mobileconfig file, and verify they lead to a root CA found on the
system. If they do, this root CA will be used as the CACert in
the provisioning file.
This commit is contained in:
James Prestwood 2019-07-26 14:40:58 -07:00 committed by Denis Kenzior
parent 1fdea9b2d3
commit d8dac9a330
1 changed files with 402 additions and 0 deletions

402
tools/ios_convert.py Executable file
View File

@ -0,0 +1,402 @@
#!/usr/bin/python3
import subprocess
import sys
import os
import argparse
from xml.etree import ElementTree
class Network:
def __init__(self):
self.eap_types = []
self.outer_id = None
self.inner_eap = None
self.nai_realms = []
self.cert_path = None
self.is_hotspot = False
self.username = None
self.password = None
self.ssid = None
def process_eap_config(eap_conf, network):
for m in range(len(eap_conf)):
if eap_conf[m].text == "AcceptEAPTypes":
tarray = eap_conf[m + 1]
for i in range(len(tarray)):
network.eap_types.append(tarray[i].text)
elif eap_conf[m].text == "TTLSInnerAuthentication":
network.inner_eap = eap_conf[m + 1].text
elif eap_conf[m].text == "OuterIdentity":
network.outer_id = eap_conf[m + 1].text
elif eap_conf[m].text == "UserName" and args.user:
network.username = eap_conf[m + 1].text
elif eap_conf[m].text == "UserPassword" and args.passwd:
network.password = eap_conf[m + 1].text
def process_payload_array(parray):
network = Network()
for l in range(len(parray)):
if parray[l].text == "NAIRealmNames":
nai_array = parray[l + 1]
for i in range(len(nai_array)):
network.nai_realms.append(nai_array[i].text)
continue
elif parray[l].text == "IsHotspot":
if parray[l + 1].text == "True":
network.is_hotspot = True
else:
network.is_hotspot = False
elif parray[l].text == "SSID_STR":
network.ssid = parray[l + 1].text
elif parray[l].text == "EAPClientConfiguration":
process_eap_config(parray[l + 1], network)
return network
def process_payload(payload):
networks = []
for k in range(len(payload)):
if payload[k].tag != "dict":
continue
n = process_payload_array(payload[k])
if n:
networks.append(n)
return networks
def write_network(network, root_ca_path):
global cert_path
output = ""
eap = None
# TODO: Handle multiple EAP types?
if len(network.eap_types) < 1:
print("Not configuring open network %s" % network.ssid)
return
if network.eap_types[0] == '21':
eap = 'TTLS'
elif network.eap_types[0] == '25':
eap = 'PEAP'
if not eap:
print("TTLS or PEAP config was not found in XML")
return
if not network.inner_eap:
print("No inner EAP method found in XML")
return
if network.is_hotspot and len(network.nai_realms) == 0:
print("No NAI realms found in XML")
return
output = "[Security]\n"
output += "EAP-Method=%s\n" % eap
# Use OuterIdentity if specified. But if not use "anonymous". Some AP's
# do not like an empty identity packet and will timeout.
if network.outer_id:
output += "EAP-Identity=%s\n" % network.outer_id
else:
output += "EAP-Identity=anonymous\n"
if root_ca_path:
output += "EAP-%s-CACert=%s\n" % (eap, root_ca_path)
output += "EAP-%s-Phase2-Method=Tunneled-%s\n" % \
(eap, network.inner_eap)
if network.username:
output += "EAP-%s-Phase2-Identity=%s\n" % \
(eap, network.username)
if network.password:
output += "EAP-%s-Phase2-Password=%s\n" % \
(eap, network.password)
if network.is_hotspot:
conf_file = iwd_dir + '/hotspot/' + \
os.path.splitext(args.input)[0] + '.conf'
output += "[Hotspot]\n"
output += "NAIRealmNames="
for i in range(len(network.nai_realms)):
output += network.nai_realms[i]
if i < len(network.nai_realms) - 1:
output += ','
else:
conf_file = iwd_dir + '/' + network.ssid + '.8021x'
# Some AP's require older protocol versions. There should be no harm in
# setting this all the time.
output += "[EAPoL]\n"
output += "ProtocolVersion=1\n"
output += "\n"
print("Provisioning network %s\n" % conf_file)
if args.verbose:
print(output)
with open(conf_file, 'w+') as f:
f.write(output)
def find_root_ca(chain):
def cleanup(certs):
for c in certs:
os.remove(c)
def parse_cert_chain(file):
certs = []
in_cert = False
current = ""
f = open(file, 'r')
for l in f:
if '-----BEGIN CERTIFICATE-----' in l:
if in_cert:
print("invalid BEGIN")
exit()
in_cert = True
elif '-----END CERTIFICATE-----' in l:
if not in_cert:
print("invalid END")
exit()
in_cert = False
current += l
certs.append(current)
current = ""
continue
current += l
return certs
def find_root_ca_path(hash):
path = '/etc/ssl/certs/%s.0' % hash
if os.path.exists(path):
return os.path.realpath(path)
return None
#
# Parse each cert out of the chain.
#
certs = parse_cert_chain(chain)
files = []
subjects = []
self_signed = None
if len(certs) < 1:
print("No certs found")
exit()
#
# Write each cert into an intermediate#.pem file, get subject and see if
# any are self-signed (Root CA). If one is self signed, save this file
# to be used later when verifying the chain.
#
for index, c in enumerate(certs):
with open("/tmp/intermediate" + str(index) + ".pem", 'w+') as f:
f.write(c)
files.append("/tmp/intermediate%d.pem" % index)
with open(os.devnull, 'w') as devnull:
proc = subprocess.Popen(['openssl', 'x509', '-subject',
'-hash', '-issuer_hash', '-noout', '-in',
'/tmp/intermediate%d.pem' % index],
stdout=subprocess.PIPE, stderr=devnull)
result, err = proc.communicate()
results = result.decode("utf-8").split('\n')
sub = results[0]
own_hash = results[1]
issuer_hash = results[2]
#
# Get rid of "depth=#" openssl output
#
sub = sub[sub.index('C ='):].strip()
subjects.append(sub)
if own_hash == issuer_hash:
self_signed = '/tmp/intermediate%d.pem' % index
#
# Let openssl verify and print the subject of the cert chain. If there
# is a self signed cert we want openssl to bypass looking in the cert
# store (-no-CApath), and provide this CA (-CAfile). Otherwise, let
# openssl verify the chain as usual.
#
if not self_signed:
with open(os.devnull, 'w') as devnull:
proc = subprocess.Popen(['openssl', 'verify',
'-show_chain', '-untrusted', chain,
'/tmp/intermediate0.pem'],
stdout=subprocess.PIPE, stderr=devnull)
else:
with open(os.devnull, 'w') as devnull:
proc = subprocess.Popen(['openssl', 'verify',
'-show_chain', '-no-CApath', '-CAfile',
self_signed, '-untrusted', chain,
'/tmp/intermediate0.pem'],
stdout=subprocess.PIPE, stderr=devnull)
results, err = proc.communicate()
results = results.decode("utf-8").strip().split('\n')
#
# We only want lines starting with depth=
#
results = [e for e in results if e.startswith('depth=')]
print("Found %d certs in chain" % len(results))
#
# Get rid of prepended "depth=#"
#
ca_sub = results[-1]
ca_sub = ca_sub[ca_sub.index('C ='):].strip()
#
# The last cert in the chain will either be a Root CA, or issued by a
# Root CA. If we find a matching subject in our previous -show_chain
# command we know there is a Root CA in the chain, if not we need to
# find the Root CA on the system.
#
ca_cert_index = len(results) - 2
for i, sub in enumerate(subjects):
if ca_sub == sub:
print("Root CA found in chain, index=%d" % i)
ca_cert_index = i
#
# Now that we have this last cert, check if its a Root CA or not by
# checking if its hash matches the issuer hash.
#
with open(os.devnull, 'w') as devnull:
proc = subprocess.Popen(['openssl', 'x509', '-hash',
'-issuer_hash', '-noout', '-in',
'/tmp/intermediate%d.pem' % ca_cert_index],
stdout=subprocess.PIPE, stderr=devnull)
hashes, err = proc.communicate()
hashes = hashes.decode('utf-8').strip().split('\n')
own = hashes[0]
issuer = hashes[1]
if own == issuer:
#
# Since this is a Root CA, we should already have a copy on the
# system. Verify the hash links to a system cert.
#
path = find_root_ca_path(issuer)
if path is not None:
print("Verified Root CA exists: %s" % path)
else:
print("Root CA in chain could not be found on system")
return None
cleanup(files)
return path
print("Root CA not found in cert chain, looking on system")
#
# The final cert in the chain was not a Root CA, check if we have a Root
# CA on the system matching the last certs issuer hash.
#
path = find_root_ca_path(issuer)
if path is None:
print("Could not find issuer %s" % issuer)
return None
with open(os.devnull, 'w') as devnull:
proc = subprocess.Popen(['openssl', 'x509', '-hash',
'-issuer_hash', '-noout', '-in', path],
stdout=subprocess.PIPE, stderr=devnull)
hashes, err = proc.communicate()
hashes = hashes.decode('utf-8').strip().split('\n')
own = hashes[0]
issuer = hashes[1]
if own == issuer:
path = find_root_ca_path(issuer)
if path is not None:
print("Verified Root CA exists: %s" % path)
else:
print("Root CA could not be found on system")
return None
cleanup(files)
return path
iwd_dir='/var/lib/iwd'
cert_path = None
description = '''
Convert iOS mobileconfig file to IWD format. Currently only TTLS and PEAP are
supported. Inner methods supported are PAP, CHAP, MSCHAP, MSCHAPv2.
'''
parser = argparse.ArgumentParser(description=description)
parser.add_argument('-i', '--input', nargs='?', required=True,
metavar='mobileconfig', help='iOS mobileconfig file')
parser.add_argument('-o', '--iwd-out', nargs='?', metavar='dir',
help='IWD configuration directory (default /var/lib/iwd)')
parser.add_argument('-u', '--user', action='store_true',
help='Store username in provisioning file')
parser.add_argument('-p', '--passwd', action='store_true',
help='Store password (plaintext) in provisioning file')
parser.add_argument('-v', '--verbose', action='store_true',
help='Enable verbose output')
args = parser.parse_args()
if args.iwd_out:
iwd_dir = args.iwd_out
with open(os.devnull, 'w') as devnull:
proc = subprocess.Popen(['openssl', 'cms', '-in', args.input, '-inform',
'der', '-verify', '-noverify'],
stdout=subprocess.PIPE, stderr=devnull)
xml, err = proc.communicate()
if args.verbose:
print(xml.decode("utf-8"))
subprocess.call(['openssl', 'cms', '-in', args.input, '-inform', 'der',
'-outform', 'pem', '-noout', '-cmsout', '-certsout',
'/tmp/certchain.crt'])
xml = xml.decode('utf-8')
root = ElementTree.fromstring(xml)
for i in range(len(root)):
for j in range(len(root[i])):
if root[i][j].text != "PayloadContent":
continue
if (root[i][j + 1].tag != "array"):
continue
payload = root[i][j + 1]
nets = process_payload(payload)
root_ca_path = find_root_ca('/tmp/certchain.crt')
for n in nets:
write_network(n, root_ca_path)