Skip to content

Instantly share code, notes, and snippets.

@trexx
Last active March 13, 2023 11:23
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save trexx/7aa763b2432f0b0952e56a1822845aa0 to your computer and use it in GitHub Desktop.
Save trexx/7aa763b2432f0b0952e56a1822845aa0 to your computer and use it in GitHub Desktop.
The .ovpn to .onc converter
#!/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()
@williamwilkerson2
Copy link

I don't know if this script will support the module OpenSSL.

@kevdogg
Copy link

kevdogg commented Mar 4, 2023

I cant get the -----BEGIN OpenVPN Static key V1----- included in the onc file.

@trexx
Copy link
Author

trexx commented Mar 13, 2023

@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.

# Example section of in-line ovpn file

key-direction 1
<tls-auth>
#
# 2048 bit OpenVPN static key
#
-----BEGIN OpenVPN Static key V1-----
073b0025464cdeaa6189247397d0f2f6
4c2cb415f7b662af421d3ea7c9d50c10
61ebd5ed93d04c2f863b4a6cc4ce6b32
b981297a1eb35d83e75b3051b162c286
653032398c3bc539bec746c778d67c16
dad74a45ce4e85e57bb04b3675f43ecc
e020210c3d252957e86b087804338c3a
2cec5f08306d276a54558cff885a7296
330ce026485ae88a0099430002a570f1
20b774bf64501ae28ed6650a2bc463ce
032a4c9495dd2849550ad09af18cb953
8aa516354e7a6f302fb7d9f66d1dad7f
9fe7683d84dd90d0985dff7dc2881b24
87884d98ffaafecff27d10d554e2f5a7
78226ee0561cb8f815a10b132b097579
9a9a92359aa0574a95715a1df0e51484
-----END OpenVPN Static key V1-----
</tls-auth>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment