Last active
March 13, 2023 11:23
-
-
Save trexx/7aa763b2432f0b0952e56a1822845aa0 to your computer and use it in GitHub Desktop.
The .ovpn to .onc converter
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
#!/usr/bin/python | |
# The .ovpn to .onc converter | |
# This tool parses an 'inline' OpenVPN profile (certs and keys are included within the .ovpn file) | |
## and spits out a Open Network Configuration file which can be imported in to ChromeOS. | |
# Open Network Configuration specs can be found here | |
## https://chromium.googlesource.com/chromium/src/+/master/components/onc/docs/onc_spec.md | |
# Original credit goes to Steve Woodrow (https://github.com/woodrow) | |
# Modified from: https://gist.github.com/woodrow/155b6af97fdbd217ddca3f9f919ee02a | |
# Modified by: Turan Asikoglu | |
import argparse, json, re, sys, uuid, base64 | |
from os.path import basename, splitext | |
from OpenSSL.crypto import PKCS12, FILETYPE_PEM, load_certificate, load_privatekey | |
class OpenVPNNetworkConfiguration(object): | |
KNOWN_CONFIG_KEYS = { | |
'name': {'key': 'Name'}, | |
'startonopen': {'key': 'VPN.AutoConnect', 'value': lambda x: bool(x)}, | |
'remote': {'key': 'VPN.Host', 'value': lambda x: x.split()[0]}, | |
'auth': {'key': 'VPN.OpenVPN.Auth'}, | |
'auth-retry': {'key': 'VPN.OpenVPN.AuthRetry'}, | |
'auth-nocache': {'key': 'VPN.OpenVPN.AuthNoCache', 'value': True}, | |
'cipher': {'key': 'VPN.OpenVPN.Cipher'}, | |
'comp-lzo': {'key': 'VPN.OpenVPN.CompLZO'}, | |
'key-direction': {'key': 'VPN.OpenVPN.KeyDirection'}, | |
'ns-cert-type': {'key': 'VPN.OpenVPN.NsCertType'}, | |
'port': {'key': 'VPN.OpenVPN.Port', 'value': lambda x: int(x)}, | |
'proto': {'key': 'VPN.OpenVPN.Proto'}, | |
'push-peer-info': {'key': 'VPN.OpenVPN.PushPeerInfo', 'value': True}, | |
'remote-cert-eku': {'key': 'VPN.OpenVPN.RemoteCertEKU'}, | |
'remote-cert-ku': {'key': 'VPN.OpenVPN.RemoteCertEKU', 'value': lambda x: x.split()}, | |
'remote-cert-tls': {'key': 'VPN.OpenVPN.RemoteCertTLS'}, | |
'reneg-sec': {'key': 'VPN.OpenVPN.RenegSec', 'value': lambda x: int(x)}, | |
'server-poll-timeout': {'key': 'VPN.OpenVPN.ServerPollTimeout', 'value': lambda x: int(x)}, | |
'shaper': {'key': 'VPN.OpenVPN.Shaper', 'value': lambda x: int(x)}, | |
'static-challenge': {'key': 'VPN.OpenVPN.StaticChallenge', 'value': lambda x: x.rsplit(maxsplit=1)}, | |
'tls-remote': {'key': 'VPN.OpenVPN.TLSRemote'}, | |
'username': {'key': 'VPN.OpenVPN.Username'}, | |
'verb': {'key': 'VPN.OpenVPN.Verb'}, | |
'verify-hash': {'key': 'VPN.OpenVPN.VerifyHash'}, | |
'verify-x509-name': { | |
'key': 'VPN.OpenVPN.VerifyX509', 'value': lambda x: {i[0]: i[1] for i in zip(['Name', 'Type'], x.split())} | |
}, | |
} | |
OPENVPN_KEYS = [ | |
'Auth', | |
'AuthRetry', | |
'AuthNoCache', | |
'Cipher', | |
'ClientCertRef', | |
'ClientCertPattern', | |
'ClientCertType', | |
'Compress', | |
'IgnoreDefaultRoute', | |
'KeyDirection', | |
'NsCertType', | |
'Password', | |
'Port', | |
'Proto', | |
'PushPeerInfo', | |
'RemoteCertEKU', | |
'RemoteCertKU', | |
'RemoteCertTLS', | |
'RenegSec', | |
'SaveCredentials', | |
'ServerCARefs', | |
'ServerCertRef', | |
'ServerPollTimeout', | |
'Shaper', | |
'StaticChallenge', | |
'TLSAuthContents', | |
'TLSRemote', | |
'Username', | |
'Verb', | |
'VerifyHash', | |
'VerifyX509', | |
] | |
def __init__(self): | |
self.openvpn = { | |
'IgnoreDefaultRoute': True, | |
'UserAuthenticationType': 'Password', | |
'ClientCertType': 'None' | |
} | |
self.vpn = { | |
'Type': 'OpenVPN', | |
'Host': None, | |
'OpenVPN': self.openvpn | |
} | |
self.network_config = { | |
'GUID': str(uuid.uuid4()), | |
'Name': None, | |
'Type': 'VPN', | |
'VPN': self.vpn | |
} | |
self.onc = { | |
'Type': 'UnencryptedConfiguration', | |
'NetworkConfigurations': [self.network_config], | |
'Certificates': [] | |
} | |
def init_with_file(self, f): | |
lines = f.readlines() | |
lines = self.transform_config(lines) | |
self.parse_openvpn_config(lines) | |
@staticmethod | |
def transform_config(lines): | |
_lines = [] | |
multiline = None | |
for line in lines: | |
if not multiline and re.match(r'\A\s*<[a-z-]+>', line): | |
multiline = line.strip() | |
elif multiline and re.match(r'\A\s*</[a-z-]+>', line): | |
_lines.append(multiline + '\n' + line.strip()) | |
multiline = None | |
elif multiline: | |
multiline += ('\n' + line.strip()) | |
else: | |
parts = line.strip().split(' ', 1) | |
command = parts[0] | |
args = None | |
if len(parts) > 1: | |
args = parts[1] | |
if command == 'remote' and len(args.split()) == 3: | |
args = args.split() | |
_lines.append('remote {}'.format(args[0])) | |
_lines.append('port {}'.format(args[1])) | |
_lines.append('proto {}'.format(args[2])) | |
else: | |
_lines.append(line.strip()) | |
return _lines | |
def parse_openvpn_config(self, lines): | |
clientx509_cert = None | |
clientx509_key = None | |
for line in lines: | |
if re.match(r'\A\s*#viscosity', line): | |
line = re.match(r'\A\s*#viscosity(.*)', line).group(1) | |
elif re.match(r'\A\s*#', line): | |
continue | |
parts = line.strip().split(' ', 1) | |
command = parts[0] | |
args = None | |
if len(parts) > 1: | |
args = parts[1] | |
if command in self.KNOWN_CONFIG_KEYS: | |
spec = self.KNOWN_CONFIG_KEYS[command] | |
value = spec.get('value', args) | |
if callable(value): | |
value = value(args) | |
_dict = self.network_config | |
key_parts = spec['key'].split('.') | |
for k in key_parts[:-1]: | |
_dict = _dict[k] | |
_dict[key_parts[-1]] = value | |
elif command.startswith('<tls-auth>'): | |
self.openvpn['TLSAuthContents'] = '\n'.join( | |
re.search('<tls-auth>(.*?)</tls-auth>', line, re.DOTALL).group(1).strip().split('\n')) | |
elif command.startswith('<ca>'): | |
_cas = [] | |
_ca_uuids = [] | |
for ca in re.findall(r'-----BEGIN CERTIFICATE-----(.*?)-----END CERTIFICATE-----', line, re.DOTALL): | |
guid = str(uuid.uuid4()) | |
_ca = ''.join(ca.strip().split()) | |
_cas.append(_ca) | |
_ca_uuids.append(guid) | |
self.onc['Certificates'].append({ | |
'GUID': guid, | |
'Type': 'Authority', | |
'X509': _ca | |
}) | |
self.openvpn['ServerCARefs'] = [_ca_uuids[0]] | |
elif command.startswith('<cert>'): | |
clientx509_cert = load_certificate(FILETYPE_PEM, re.search(r'-----BEGIN CERTIFICATE-----(.*?)-----END CERTIFICATE-----', line, re.DOTALL).group(0)) | |
elif command.startswith('<key>'): | |
clientx509_key = load_privatekey(FILETYPE_PEM, re.search(r'-----BEGIN PRIVATE KEY-----(.*?)-----END PRIVATE KEY-----', line, re.DOTALL).group(0)) | |
if clientx509_cert and clientx509_key: | |
clientp12 = PKCS12() | |
clientp12.set_certificate(clientx509_cert) | |
clientp12.set_privatekey(clientx509_key) | |
clientp12_b64 = base64.b64encode(clientp12.export()).decode('ascii') | |
guid = str(uuid.uuid4()) | |
self.onc['Certificates'].append({ | |
'GUID': guid, | |
'Type': 'Client', | |
'PKCS12': clientp12_b64 | |
}) | |
self.openvpn['ClientCertType'] = 'Ref' | |
self.openvpn['ClientCertRef'] = guid | |
def to_json(self, outfile=None): | |
if outfile: | |
json.dump(self.onc, outfile, sort_keys=True, indent=2, separators=(',', ': ')) | |
else: | |
return json.dumps(self.onc, sort_keys=True, indent=2, separators=(',', ': ')) | |
def main(): | |
parser = argparse.ArgumentParser() | |
parser.add_argument( | |
'--infile', | |
metavar='OPENVPN_CONFIG_FILE', | |
help='OpenVPN config file to be converted. If not present, stdin is used.', | |
default=None | |
) | |
parser.add_argument( | |
'--outfile', | |
metavar='ONC_FILE', | |
help='Path to output ONC file. If not present, stdout is used.', | |
default=None | |
) | |
args = parser.parse_args() | |
infile = sys.stdin | |
outfile = sys.stdout | |
if args.infile: | |
infile = open(args.infile, 'r') | |
vpnname = splitext(basename(args.infile))[0] | |
if args.outfile: | |
outfile = open(args.outfile, 'w') | |
c = OpenVPNNetworkConfiguration() | |
c.init_with_file(infile) | |
c.network_config['Name'] = vpnname | |
c.to_json(outfile) | |
infile.close() | |
outfile.close() | |
if __name__ == '__main__': | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
@kevdogg Its been a while since this was last used by me.
IIRC, you need to ensure the static key is in-line in the ovpn file like so. A path referring to the file will not work.
You also need to mention the key direction too when using this in-line format by using
key-direction
Apologies if you're already doing this and I'm afraid I don't use this anymore to be able to investigate.