Last active
December 6, 2023 10:23
-
-
Save bennet0496/b1713a3a2bee909f213eedce7762a5ef to your computer and use it in GitHub Desktop.
EAP-TLS mobileconfig for Apple macOS iOS iPadOS
This file contains 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
import os | |
import plistlib | |
import uuid | |
from optparse import OptionParser, OptionGroup | |
import socket | |
from cryptography import x509 | |
from cryptography.hazmat.primitives import hashes | |
from cryptography.hazmat.primitives import serialization | |
from cryptography.hazmat.primitives.serialization import PrivateFormat, load_pem_private_key, pkcs12, pkcs7 | |
from getpass import getpass | |
# | |
# ./mobileconfig --root=... --intermediate=... --private=... {--ethernet|--wifi --ssid=... --wpa=...} | |
# --globaldesc=... --globalname=... --conndesc=... --connname=... | |
if __name__ == "__main__": | |
parser = OptionParser() | |
parser.add_option("-o", "--out", action="store", dest="out", metavar="FILE", | |
help="Output path for mobileconfig. Defaults to stdout") | |
parser.add_option("--globaldesc", action="store", type="string", dest="globaldesc", | |
help="Set the global payload description") | |
parser.add_option("--globalname", action="store", type="string", dest="globalname", | |
help="Set the global payload name") | |
parser.add_option("--globalidentifier", action="store", type="string", dest="globalidentifier", | |
help="Set the global payload identifier") | |
parser.add_option("--conndesc", action="store", type="string", dest="conndesc", | |
help="Set the connection payload description") | |
parser.add_option("--connname", action="store", type="string", dest="connname", | |
help="Set the connection payload name") | |
parser.add_option("--batch", action="store_true", dest="batch", | |
help="Run in batch mode. Don't prompt for anything") | |
parser.add_option("--sign", action="store", dest="sign", metavar="FILE", | |
help="Sign the resulting profile with the PKCS#12 private key and certificate") | |
parser.add_option("--sign-cert", action="store", dest="sign_cert", metavar="FILE", | |
help="Sign the resulting profile with the PEM certificate and key below") | |
parser.add_option("--sign-key", action="store", dest="sign_key", metavar="FILE", | |
help="Sign the resulting profile with the PEM key and certificate above") | |
parser.add_option("--sign-pass", action="store", dest="sign_pass", metavar="PASSWORD", | |
help="Password for the signing key") | |
group = OptionGroup(parser, "Authentication and Certificate Options") | |
group.add_option("-r", "--root", action="append", type="string", dest="roots", | |
metavar="FILE", help="Set root CA file(s). Option can be called multiple times to add multiple files") | |
group.add_option("-i", "--intermediate", action="append", type="string", dest="intermediates", | |
metavar="FILE", help="Set intermediate CA file(s). Option can be called multiple times to add multiple files") | |
group.add_option("-p", "--private", action="store", type="string", dest="private", | |
metavar="FILE", help="Set private key file") | |
group.add_option("-c", "--cert", action="store", type="string", dest="cert", | |
metavar="FILE", help="Set user certificate file (only required with PKCS#1 or if PKCS#12 does not contain a certificate)") | |
group.add_option("--pkcs12", action="store_true", dest="private_p12", | |
help="Private key file is PKCS#12 (default)") | |
group.add_option("--pkcs1", "--pem", action="store_true", dest="private_pem", | |
help="Private key file is PKCS#1 (DER) or PEM encoded") | |
group.add_option("--cert-passin", action="store", dest="passin", metavar="PASSWORD", | |
help="Certificate/key import password") | |
group.add_option("--cert-passout", action="store", dest="passout", metavar="PASSWORD", | |
help="Certificate/key export password") | |
group.add_option("--cert-passin-from-env", action="store_true", dest="passin_env", | |
help="Read certificate/key import password from CERT_PASS_IN environment variable") | |
group.add_option("--cert-passout-from-env", action="store_true", dest="passout_env", | |
help="Read certificate/key export password from CERT_PASS_OUT environment variable") | |
group.add_option("-u", "--user", action="store", dest="user", metavar="USERNAME", | |
help="Set the username or identity") | |
group.add_option("-s", "--subjAltMatch", "--server", "--TLSTrustedServerName", action="append", type="string", dest="servers", | |
metavar="SERVER", help="Add a trusted server name or subject alternate name match") | |
parser.add_option_group(group) | |
group = OptionGroup(parser, "Connection Options") | |
group.add_option("-e","--ethernet", action="store_true", dest="ethernet", | |
help="Configure an ethernet connection") | |
group.add_option("-w", "--wifi", action="store_true", dest="wifi", | |
help="Configure a WiFi connection") | |
group.add_option("--ssid", action="store", type="string", dest="ssid", | |
help="Set WiFi SSID") | |
group.add_option("--wpa", action="store", type="choice", dest="wpa", choices=["1", "2", "3"], | |
help="Set WPA level 1/2/3 ") | |
parser.add_option_group(group) | |
(options, args) = parser.parse_args() | |
# print(options, args) | |
rev_host = socket.getfqdn().split(".") | |
rev_host.reverse() | |
rev_host = ".".join(rev_host) | |
PAYLOAD = { | |
"PayloadDisplayName": options.globalname or "EAP TLS Client Configuration Profile", | |
"PayloadDescription": options.globaldesc or "Configured TLS Certificate Credentials for EAP-TLS Authentication", | |
"PayloadIdentifier": options.globalidentifier or rev_host + "." + str(uuid.uuid4()), | |
"PayloadRemovalDisallowed": False, | |
"PayloadType": "Configuration", | |
"PayloadUUID": str(uuid.uuid4()), | |
"PayloadVersion": 1, | |
"PayloadContent": [] | |
} | |
cert_counter = 1 | |
cert_uuids = [] | |
for cert in options.roots: | |
x509_cert = x509.load_pem_x509_certificate(open(cert, 'rb').read()) | |
# print(x509_cert.issuer, x509_cert.subject) | |
if x509_cert.issuer != x509_cert.subject: | |
raise ValueError("{0} is not a root certificate (issuer does not match subject)".format(cert)) | |
puuid = str(uuid.uuid4()) | |
cert_uuids.append(puuid) | |
PAYLOAD["PayloadContent"].append({ | |
"PayloadDisplayName": "Identity Provider CA #{} (Root)".format(cert_counter), | |
"PayloadDescription": "The Identity Provider Certification Authority", | |
"PayloadType": "com.apple.security.root", | |
"PayloadContent": x509_cert.public_bytes(serialization.Encoding.PEM), | |
"PayloadUUID": puuid, | |
"PayloadIdentifier": "com.apple.security.root." + puuid, | |
}) | |
cert_counter += 1 | |
if options.intermediates is not None: | |
for cert in options.intermediates: | |
x509_cert = x509.load_pem_x509_certificate(open(cert, 'rb').read()) | |
# print(x509_cert.issuer, x509_cert.subject) | |
if x509_cert.issuer == x509_cert.subject: | |
raise ValueError("{0} is not a intermediate certificate (self-signed cert, issuer matches subject)" | |
.format(cert)) | |
puuid = str(uuid.uuid4()) | |
cert_uuids.append(puuid) | |
PAYLOAD["PayloadContent"].append({ | |
"PayloadDisplayName": "Identity Provider CA #{} (Intermediate)".format(cert_counter), | |
"PayloadDescription": "The Identity Provider Certification Authority", | |
"PayloadType": "com.apple.security.pkcs1", | |
"PayloadContent": x509_cert.public_bytes(serialization.Encoding.PEM), | |
"PayloadUUID": puuid, | |
"PayloadIdentifier": "com.apple.security.pkcs1." + puuid, | |
}) | |
cert_counter += 1 | |
if options.private is None: | |
raise ValueError("Private key required") | |
if options.private_p12 and options.private_pem: | |
raise ValueError("Private key format must be either PKCS#12 OR PEM/DER") | |
if options.private_pem: | |
if options.cert is None: | |
raise ValueError("Certificate required with PKCS#1 key") | |
with open(options.private, 'rb') as pkey, open(options.cert, 'rb') as cert: | |
cont = True | |
passwd = options.passin | |
if options.passin_env: | |
passwd = os.getenv("CERT_PASS_IN") | |
priv = None | |
while cont: | |
try: | |
priv = load_pem_private_key(pkey.read(), password=passwd) | |
except (TypeError, ValueError): | |
if options.batch: | |
raise | |
else: | |
print("Invalid password, please provide a password or cancel with ctrl+c") | |
passwd = getpass("Password: ") | |
except KeyboardInterrupt: | |
print("canceled by user") | |
cont = False | |
if priv is None: | |
raise ValueError("Private key required") | |
x509_cert = x509.load_pem_x509_certificate(cert.read()) | |
try: | |
if options.passout: | |
ex_pass = options.passout | |
elif options.passout_env: | |
ex_pass = os.getenv("CERT_PASS_OUT") | |
elif not options.batch: | |
ex_pass = getpass("Enter P12 export password or press crtl+c to cancel: ") | |
except KeyboardInterrupt: | |
ex_pass = None | |
if ex_pass is None: | |
print("Warning passwordless PKCS#12 might not be importable") | |
p12b = pkcs12.serialize_key_and_certificates( | |
bytes("User certificate for {}".format(options.connname or "EAP-TLS"), "ascii"), priv, x509_cert, None, ( | |
PrivateFormat.PKCS12.encryption_builder(). | |
kdf_rounds(2048). | |
key_cert_algorithm(pkcs12.PBES.PBESv1SHA1And3KeyTripleDESCBC). | |
hmac_hash(hashes.SHA1()).build(bytes(ex_pass, "utf-8")) | |
)) | |
else: | |
with open(options.private, 'rb') as f: | |
passwd = options.passin | |
if options.passin_env: | |
passwd = os.getenv("CERT_PASS_IN") | |
try: | |
p12 = pkcs12.load_pkcs12(f.read(), bytes(passwd, "utf-8")) | |
except (TypeError, ValueError): | |
if not options.batch: | |
print("Invalid password, please provide a password or skip with enter") | |
passwd = getpass("Password: ") | |
if passwd is not None: | |
p12 = pkcs12.load_pkcs12(f.read(), bytes(passwd, "utf-8")) | |
if p12 is None: | |
print("Warning cannot inspect PKCS#12. Using as is.") | |
f.seek(0) | |
p12b = f.read() | |
else: | |
try: | |
if options.passout: | |
ex_pass = options.passout | |
elif options.passout_env: | |
ex_pass = os.getenv("CERT_PASS_OUT") | |
elif not options.batch: | |
ex_pass = getpass("Enter P12 export password or press crtl+c to cancel: ") | |
except KeyboardInterrupt: | |
ex_pass = None | |
if ex_pass is None: | |
print("Warning passwordless PKCS#12 might not be importable") | |
# print(p12, p12.cert) | |
p12b = pkcs12.serialize_key_and_certificates( | |
bytes("User certificate for {}".format(options.connname or "EAP-TLS"), "ascii"), p12.key, p12.cert.certificate, p12.additional_certs, ( | |
PrivateFormat.PKCS12.encryption_builder(). | |
kdf_rounds(2048). | |
key_cert_algorithm(pkcs12.PBES.PBESv1SHA1And3KeyTripleDESCBC). | |
hmac_hash(hashes.SHA1()).build(bytes(ex_pass, "utf-8")) | |
)) | |
p12uuid = str(uuid.uuid4()) | |
PAYLOAD["PayloadContent"].append({ | |
"PayloadDisplayName": "User certificate for {}".format(options.connname or "EAP-TLS"), | |
"PayloadDescription": "Adds user certificate and private key for EAP-TLS with {}".format(options.connname), | |
"PayloadType": "com.apple.security.pkcs12", | |
"PayloadContent": p12b, | |
"AllowAllAppsAccess": False, | |
"PayloadUUID": p12uuid, | |
"PayloadIdentifier": "com.apple.security.pkcs12." + p12uuid, | |
}) | |
if options.user is None: | |
raise ValueError("Username cannot be None") | |
EAP_PAYLOAD_FRAG = { | |
"UserName": options.user, | |
"AcceptEAPTypes": [13], | |
"OneTimePassword": False, | |
"PayloadCertificateAnchorUUID": cert_uuids, | |
"TLSTrustedServerNames": options.servers, | |
"TLSAllowTrustExceptions": False, | |
"TLSCertificateIsRequired": True, | |
"TLSMinimumVersion": "1.2", | |
"TLSMaximumVersion": "1.2" | |
} | |
if (options.ethernet and options.wifi) or (options.ethernet is None and options.wifi is None): | |
raise ValueError("Can only select Ethernet OR Wifi, not both or none") | |
connuuid = str(uuid.uuid4()) | |
if options.ethernet: | |
PAYLOAD["PayloadContent"].append({ | |
"PayloadDisplayName": options.connname or "EAP-TLS Ethernet Configuration", | |
"PayloadDescription": options.conndesc or "Configures TLS Certificate Credentials for EAP-TLS Ethernet with 802.1X", | |
"PayloadType": "com.apple.globalethernet.managed", | |
"Interface": "AnyEthernet", | |
"PayloadCertificateUUID": p12uuid, | |
"EAPClientConfiguration": EAP_PAYLOAD_FRAG, | |
"PayloadUUID": connuuid, | |
"PayloadIdentifier": "com.apple.globalethernet.managed." + connuuid, | |
}) | |
if options.wifi: | |
if options.ssid is None: | |
raise ValueError("SSID required in WiFi mode") | |
if options.wpa is None: | |
raise ValueError("WPA mode required in WiFi mode") | |
PAYLOAD["PayloadContent"].append({ | |
"PayloadDisplayName": options.connname or "EAP-TLS WiFi Configuration", | |
"PayloadDescription": options.conndesc or "Configures TLS Certificate Credentials for EAP-TLS Enterprise WiFi", | |
"PayloadType": "com.apple.wifi.managed", | |
"SSID_STR": options.ssid, | |
"HIDDEN_NETWORK": False, | |
"AutoJoin": True, | |
"EncryptionType": "WPA" + options.wpa, | |
"IsHotspot": False, | |
"CaptiveBypass": True, | |
"ProxyType": "None", | |
"PayloadCertificateUUID": p12uuid, | |
"EAPClientConfiguration": EAP_PAYLOAD_FRAG, | |
"PayloadUUID": connuuid, | |
"PayloadIdentifier": "com.apple.globalethernet.managed." + connuuid, | |
}) | |
plist_xml = plistlib.dumps(PAYLOAD) | |
if options.sign: | |
with open(options.sign, 'rb') as f: | |
p12 = pkcs12.load_pkcs12(f.read(), options.sign_pass and bytes(options.sign_pass, "utf-8")) | |
data = pkcs7.PKCS7SignatureBuilder().set_data(plist_xml.translate(None,b"\t\n\r")).add_signer( | |
p12.cert.certificate, p12.key, hashes.SHA256()).sign(serialization.Encoding.DER, []) | |
elif options.sign_key and options.sign_cert: | |
with open(options.sign_key, 'rb') as k, open(options.sign_cert, 'rb') as c: | |
cert = x509.load_pem_x509_certificate(c.read()) | |
key = serialization.load_pem_private_key(k.read(), options.sign_pass and bytes(options.sign_pass, "utf-8")) | |
data = pkcs7.PKCS7SignatureBuilder().set_data(plist_xml.translate(None,b"\t\n\r")).add_signer( | |
cert, key, hashes.SHA256()).sign(serialization.Encoding.DER, []) | |
else: | |
data = plist_xml | |
if options.out and options.out != "-": | |
with open(options.out, 'wb') as f: | |
f.write(data) | |
else: | |
print(data.decode('utf-8', errors='ignore')) | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment