-
-
Save ArturKlauser/ed5af568ed9ccf9be1c0a1ffb833587d to your computer and use it in GitHub Desktop.
The .ovpn to .onc converter
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
#!/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 | |
# | |
# Adapted from https://www.snip2code.com/Snippet/3217054/The--ovpn-to--onc-converter | |
# (same as https://gist.github.com/trexx/7aa763b2432f0b0952e56a1822845aa0) | |
# Modified by: [email protected] | |
import argparse | |
import base64 | |
import json | |
import re | |
import sys | |
import uuid | |
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'}, | |
'comp-noadapt': {'key': 'VPN.OpenVPN.CompNoAdapt', 'value': True}, | |
'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.RemoteCertKU', '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())} | |
}, | |
} | |
def __init__(self): | |
self.openvpn = {} | |
self.vpn = { | |
'Type': 'OpenVPN', | |
'AutoConnect': False, | |
'Host': None, | |
'OpenVPN': self.openvpn | |
} | |
self.network_config = { | |
'GUID': str(uuid.uuid4()), | |
'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].split() | |
if command == 'remote' and len(args) >= 2: | |
_lines.append('remote {}'.format(args[0])) | |
_lines.append('port {}'.format(args[1])) | |
if len(args) >= 3: | |
_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['ServerCARef'] = _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: | |
# cat client.key client.crt | openssl pkcs12 -export -name foobar -passout pass: | base64 -w0 | |
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 | |
) | |
parser.add_argument( | |
'--name', | |
help='OpenVPN client config name. Required.', | |
required=True, | |
default=None | |
) | |
args = parser.parse_args() | |
infile = sys.stdin | |
outfile = sys.stdout | |
if args.infile: | |
infile = open(args.infile, 'r') | |
if args.outfile: | |
outfile = open(args.outfile, 'w') | |
c = OpenVPNNetworkConfiguration() | |
c.init_with_file(infile) | |
c.network_config['Name'] = args.name | |
c.openvpn['Username'] = 'not_used' | |
c.openvpn['Password'] = 'not_used' | |
c.openvpn['UserAuthenticationType'] = 'None' | |
c.openvpn['SaveCredentials'] = True | |
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