Skip to content

Instantly share code, notes, and snippets.

@bennet0496
Last active December 6, 2023 10:23
Show Gist options
  • Save bennet0496/b1713a3a2bee909f213eedce7762a5ef to your computer and use it in GitHub Desktop.
Save bennet0496/b1713a3a2bee909f213eedce7762a5ef to your computer and use it in GitHub Desktop.
EAP-TLS mobileconfig for Apple macOS iOS iPadOS
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