So after having a bit of a play around and cobbling this together using code from a few other projects (mostly the CloudLAPS project and a few python crypto library examples), we have a proof of concept certificate signing and installation system for Ziti identity certs.
It uses an Azure function (should be cheap to run) and a private RSA key in Azure key vault to authenticate devices based on their Entra ID enrollment certificate and then issues a user and device certificate. The certs issued have no user auth EKUs (and the Ziti PKI doesn't/doesn't need to chain to our corp PKI anyway) so not really worried about it being used for privileged escalation attacks (but they code probably needs a proper audit and review before being put into prod regardless).
I'm not sure if we will be continuing with the separate device and user tunnel identities approach into prod, but it is simple enough to remove the extra operations.
One gotcha we did find was that we couldn't enroll 2 identities with the same CN, so we may also need to code to handle i.e a user changing a device.
import json
import os
import asyncio
import re
import base64
import hashlib
import base64
import typing
import datetime
import logging
import hashlib
from datetime import timedelta
from cryptography import x509
from cryptography.x509.oid import NameOID
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives.asymmetric.padding import PKCS1v15
from cryptography.hazmat.primitives._asymmetric import AsymmetricPadding
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives import _serialization
from cryptography.hazmat.primitives.asymmetric import utils as asym_utils
import azure.functions as func
from azure.identity import DefaultAzureCredential
from azure.identity import DefaultAzureCredential
from azure.keyvault.keys import KeyClient
from azure.keyvault.keys.crypto import CryptographyClient, EncryptionAlgorithm
from azure.keyvault.keys.crypto import SignatureAlgorithm
from msgraph import GraphServiceClient
#function definitions
#signs a CSR
def sign_certificate_request(csr_cert, ca_cert, private_ca_key, subjectOverride = None):
subject = csr_cert.subject
if bool(subjectOverride):
subject = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, subjectOverride)])
cert = x509.CertificateBuilder().subject_name(
subject
).issuer_name(
ca_cert.subject
).public_key(
csr_cert.public_key()
).serial_number(
x509.random_serial_number()
).not_valid_before(
datetime.datetime.now(datetime.timezone.utc)
).not_valid_after(
# Our certificate will be valid for 10 days for testing
datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(days=10)
# Sign our certificate with our private key
).sign(private_ca_key, hashes.SHA256())
# return DER certificate
return cert
#async funciton to handle graph queries, as required by Azure Python SDK
async def get_deviceData(deviceId, client):
asyncGraphResponses = {
"display_name": None,
"ownership": None,
"enabled": None,
"EntraAttributeThumbprint": None,
"EntraAttributePublicKeyHash": None,
"device_owner": None
}
try:
device = await client.devices_with_device_id(deviceId).get()
deviceOwners = await client.devices.by_device_id(device.id).registered_owners.get()
except:
#we don't throw an exception here, we let the main thread handle it
logging.warning("No response from Graph API")
return asyncGraphResponses
if device:
asyncGraphResponses["display_name"] = device.display_name
asyncGraphResponses["ownership"] = device.device_ownership
asyncGraphResponses["enabled"] = device.account_enabled
decodedKey = (base64.b64decode(re.search("key=b'(.+?)'", str(device.alternative_security_ids)).group(1)).decode("utf-8")).replace("\x00", "") #wtf
asyncGraphResponses["EntraAttributeThumbprint"] = str(decodedKey)[21:61]
asyncGraphResponses["EntraAttributePublicKeyHash"] = str(decodedKey)[61:]
asyncGraphResponses["device_owner"] = re.search("user_principal_name='(.+?)'", str(deviceOwners)).group(1) #only takes 1st owner
logging.debug("Device Name from Entra is " + asyncGraphResponses["display_name"])
logging.debug("Device ownership from Entra is " + asyncGraphResponses["ownership"])
logging.debug("Device enabled status from Entra is " + str(asyncGraphResponses["enabled"]))
logging.debug("Entra certificate thumbprint for this Device is " + asyncGraphResponses["EntraAttributeThumbprint"])
logging.debug("Entra certificate Public Key Hash for this Device is " + asyncGraphResponses["EntraAttributePublicKeyHash"])
logging.debug("Device owner from Entra is " + asyncGraphResponses["device_owner"])
return asyncGraphResponses
#check if device is the entra device it is claiming to be, and is elligible to recieve certs
def authenticateDevice(graph, device):
if not bool((graph["ownership"]).casefold() == 'Company'.casefold()):
return "Device not Company owned"
if not bool(graph["enabled"]):
return "Device Enabled"
if not bool((graph["EntraAttributeThumbprint"]).casefold() == (device["deviceCertThumbprint"]).casefold()):
return "Device Certificate Thumbprint does not match Entra"
if not bool((graph["EntraAttributePublicKeyHash"]).casefold() == (device["deviceCertPublicKeyHash"]).casefold()):
return "Device Certificate Public Key Hash does not match Entra"
if not bool((graph["display_name"]).casefold() == (device["display_name"]).casefold()):
return "Device name does not match Entra"
return None
#custom key class defintion, based on oracle cloud sample code
class AzureVaultPrivateKey(rsa.RSAPrivateKey):
def __init__(self, KVUri, key_name, credential):
self._key_id = key_name
key_client = KeyClient(vault_url=KVUri, credential=credential)
Azurekey = key_client.get_key(key_name)
self.crypto_client = CryptographyClient(Azurekey, credential=credential)
def sign(
self,
data: bytes,
padding: AsymmetricPadding,
algorithm: typing.Union[asym_utils.Prehashed, hashes.HashAlgorithm],
) -> bytes:
assert not isinstance(algorithm, asym_utils.Prehashed)
assert isinstance(padding, PKCS1v15)
if isinstance(algorithm, hashes.SHA256):
alg = SignatureAlgorithm.rs256
digest = hashlib.sha256(data).digest()
elif isinstance(algorithm, hashes.SHA384):
alg = SignatureAlgorithm.rs384
digest = hashlib.sha384(data).digest()
elif isinstance(algorithm, hashes.SHA512):
alg = SignatureAlgorithm.rs512
digest = hashlib.sha512(data).digest()
result = self.crypto_client.sign(alg, digest)
# TODO: handle errors.
return result.signature
# Every method below here is unimplemented for now but needs to be
# present to satisfy the interface.
def _key_info(self):
raise NotImplementedError()
def decrypt(self, ciphertext: bytes, padding: AsymmetricPadding) -> bytes:
raise NotImplementedError()
def key_size(self) -> int:
raise NotImplementedError()
def public_key(self) -> "rsa.RSAPublicKey":
raise NotImplementedError()
def private_numbers(self) -> "rsa.RSAPrivateNumbers":
raise NotImplementedError()
def private_bytes(
self,
encoding: _serialization.Encoding,
format: _serialization.PrivateFormat,
encryption_algorithm: _serialization.KeySerializationEncryption,
) -> bytes:
raise NotImplementedError()
app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS)
@app.route(route="signClientCSR")
def signClientCSR(req: func.HttpRequest) -> func.HttpResponse:
logging.info('Python HTTP trigger function processed a request.')
logging.info("Loading Input Data")
data = req.get_json()
#Basic input validation
inputValuesNames = ["DeviceName", "DeviceEntraCert", "UserCSR", "DeviceCSR", "UserCSRSig", "DeviceCSRSig"]
logging.info("Validating Input")
try:
for inputValuesName in inputValuesNames:
if not bool(data.get(inputValuesName)):
return func.HttpResponse(
"Request missing required value: " + inputValuesName,
status_code=400
)
raise Exception(inputValuesName + " not present")
except:
logging.critical("Input not correct")
return func.HttpResponse(
"Error reading Input: Input not Correct",
status_code=400
)
logging.info("All Inputs Prsent")
#all values must atleast exist at this point, but may not be valid
logging.debug(data.get("DeviceName"))
logging.debug(data.get("DeviceEntraCert"))
logging.debug(data.get("UserCSR"))
logging.debug(data.get("DeviceCSR"))
logging.debug(data.get("UserCSRSig"))
logging.debug(data.get("DeviceCSRSig"))
#Dictionary to hold device data from the request
deviceClaims = {
"display_name": None,
"deviceId": None,
"deviceCertPublicKeyPEM": None,
"deviceCertPublicKeyHash": None,
"deviceCertThumbprint": None
}
logging.info("Processing Input Data")
#tries to load in data from the request, including cert de-serialisation and attribute exports
try:
deviceClaims["display_name"] = data["DeviceName"]
presentedDeviceCert = x509.load_pem_x509_certificate(bytes(data["DeviceEntraCert"], "ascii"))
deviceClaims["deviceId"] = presentedDeviceCert.subject.rfc4514_string().replace('CN=', '')
deviceClaims["deviceCertPublicKeyPEM"] = str(presentedDeviceCert.public_key().public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.PKCS1
))
deviceCertPublicKeyIntermediary = str(deviceClaims["deviceCertPublicKeyPEM"]).replace(
r"b'-----BEGIN RSA PUBLIC KEY-----", ''
).replace(r'\n', ''
).replace(r"-----END RSA PUBLIC KEY-----'", '')
deviceClaims["deviceCertPublicKeyHash"] = str(base64.b64encode(hashlib.sha256(base64.b64decode(deviceCertPublicKeyIntermediary)).digest())).replace("b'", '').replace("'", '')
deviceClaims["deviceCertThumbprint"] = bytearray(presentedDeviceCert.fingerprint(hashes.SHA1())).hex()
logging.debug("Device name is " + deviceClaims["display_name"] )
logging.debug("Device certificate Device Id is " + deviceClaims["deviceId"] )
logging.debug("Device certificate Public Key is " + deviceClaims["deviceCertPublicKeyPEM"])
logging.debug("Device certificate Public Key Hash is " + deviceClaims["deviceCertPublicKeyHash"])
logging.debug("Device certificate thumbprint is " + deviceClaims["deviceCertThumbprint"])
except:
logging.critical("Device cert not Valid X509 certificate, or PyCA decided to deprecate required functionaity")
return func.HttpResponse(
"Device certificate presented is not a Valid X509 certificate",
status_code=400
)
logging.info("Input Data Processed")
#tries to connect to graph API as the current user/managed identity
logging.info("Trying to authenticate to Graph API")
try:
credential = DefaultAzureCredential() #this is also reused for keyvault later
scopes = ['https://graph.microsoft.com/.default']
client = GraphServiceClient(credentials=credential, scopes=scopes)
except:
logging.critical("Unable to Authenticate to Graph API")
return func.HttpResponse(
"Server Unable to Auth to Graph API.",
status_code=500
)
#Pulls in Data from Entra/Graph API (assuming device ID is valid
logging.info("Connecting to Graph to Query for Device Attribtues")
graphResponses = asyncio.run(get_deviceData(deviceClaims["deviceId"], client))
print("Checking Graph returned expected values")
#validates that we recieved good responses back from the async worker.
graphResponsesValuesNames = ["display_name", "ownership", "enabled", "EntraAttributeThumbprint", "EntraAttributePublicKeyHash", "device_owner"]
#Check all values exist
for graphResponsesValuesName in graphResponsesValuesNames:
if not bool(graphResponses[graphResponsesValuesName]):
logging.critical(graphResponsesValuesName + " not found")
return func.HttpResponse(
"Graph API failed to return value: " + graphResponsesValuesName,
status_code=500
)
logging.info("All graph Values Present")
logging.info("Trying to Authenticate Device")
deviceAuthenticationError = authenticateDevice(graphResponses, deviceClaims)
if bool(deviceAuthenticationError):
logging.critical("Input not correct")("Device authentication error" + deviceAuthenticationError)
return func.HttpResponse(
"Device authentication error" + deviceAuthenticationError,
status_code=401
)
#device is now authenticated
logging.info("Device is likely the device it is claiming to be")
logging.info("Checking that CSR Signatures have been signed by the authenticated device")
try:
presentedDeviceCert.public_key().verify(base64.b64decode(data["UserCSRSig"]), str(data["UserCSR"]).encode('utf-8'), padding.PKCS1v15(),hashes.SHA256())
presentedDeviceCert.public_key().verify(base64.b64decode(data["DeviceCSRSig"]), str(data["DeviceCSR"]).encode('utf-8'), padding.PKCS1v15(),hashes.SHA256())
except:
logging.critical("Input not correct")("Certificate CSRs could not be validated")
return func.HttpResponse(
"CSR Signatures don't appear to be signed by the authenticated device certificate",
status_code=401
)
logging.info("Certificate CSRs signed by the authenticated device")
logging.info("Trying to authenticate to keyVault")
KVUri = os.environ["KVURI"]
key_name = os.environ["KEY_NAME"]
try:
AzureCAPrivateKey = AzureVaultPrivateKey(KVUri, key_name, credential)
except:
logging.critical("Could not authenticate to" + KVUri + " and access " + key_name )
return func.HttpResponse(
"Could not authenticate to key vault",
status_code=500
)
logging.info("Got Access to Key")
#get CA cert
CA_CRT_PEM = os.environ["CA_CRT"]
CA_crt = x509.load_pem_x509_certificate(CA_CRT_PEM.encode('utf-8'))
logging.info("Signing Certs")
userCRT = sign_certificate_request(x509.load_pem_x509_csr(str(data["UserCSR"]).encode('utf-8')), CA_crt, AzureCAPrivateKey, subjectOverride = graphResponses["device_owner"])
deviceCRT = sign_certificate_request(x509.load_pem_x509_csr(str(data["DeviceCSR"]).encode('utf-8')), CA_crt, AzureCAPrivateKey, subjectOverride = graphResponses["display_name"])
logging.info("Certs Signed")
returnData = {
"userCert": str(userCRT.public_bytes(serialization.Encoding.PEM)).replace("b'", '').replace("'", '').replace(r"\n", '\n'),
"deviceCert": str(deviceCRT.public_bytes(serialization.Encoding.PEM)).replace("b'", '').replace("'", '').replace(r"\n", '\n'),
}
logging.debug("User Cert:")
logging.debug(returnData["userCert"])
logging.debug("Device Cert:")
logging.debug(returnData["deviceCert"])
logging.info("Exiting Succesful!")
return func.HttpResponse(json.dumps(returnData), status_code=201, mimetype="application/json")
<#
.SYNOPSIS
Installer Script for OpenZiti Client that generates local private keys and install identities. .
.DESCRIPTION
This script is based off the CloudLAPS methods for authenticating an Entra Joined device to an Azure function
An azure function is used to produce and sign CSRs for the publioc keys.
.NOTES
FileName: Install.ps1
#>
#Generates public and private keys. Private keys are written to the application's required locations, and the CSR containing the public keys are added to the request object.
function generate_keys {
param (
$data
)
#notes, will need to be updated to point to machine cert store when getting ready to deploy
#Generates Private keys within cert store and prepares CSR.
#certreq.exe is used as a wrapper due to difficulties using rsa keys or x509 certs directly within powershell and .net
certreq.exe -new .\CSR-Device.inf CSR-Device.req
certreq.exe -new .\CSR-User.inf CSR-User.req
#add csr data to request object
$data["DeviceCSR"] = (Get-Content -Path CSR-Device.req -raw).psobject.BaseObject
$data["UserCSR"] = (Get-Content -Path CSR-User.req -raw).psobject.BaseObject
#Extract Key and convert to PEM
$deviceCertificate = Get-ChildItem -Path "Cert:\LocalMachine\REQUEST" -Recurse | Where-Object { $PSItem.Subject -eq 'CN=DEVICE' }
$userCertificate = Get-ChildItem -Path "Cert:\LocalMachine\REQUEST" -Recurse | Where-Object { $PSItem.Subject -eq 'CN=USER' }
$devicePrivateKey = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($deviceCertificate)
$userPrivateKey = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($userCertificate)
$devicePrivateKeyBytes = $devicePrivateKey.Key.Export([System.Security.Cryptography.CngKeyBlobFormat]::Pkcs8PrivateBlob)
$userPrivateKeyBytes = $userPrivateKey.Key.Export([System.Security.Cryptography.CngKeyBlobFormat]::Pkcs8PrivateBlob)
$devicePrivateKeyBase64 = [System.Convert]::ToBase64String($devicePrivateKeyBytes, [System.Base64FormattingOptions]::InsertLineBreaks)
$userPrivateKeyBase64 = [System.Convert]::ToBase64String($userPrivateKeyBytes, [System.Base64FormattingOptions]::InsertLineBreaks)
$deviceKeyPem = @"
-----BEGIN PRIVATE KEY-----
$devicePrivateKeyBase64
-----END PRIVATE KEY-----
"@
$userKeyPem = @"
-----BEGIN PRIVATE KEY-----
$userPrivateKeyBase64
-----END PRIVATE KEY-----
"@
#should write to C:\Windows\System32\Config\systemprofile\AppData\Roaming\NetFoundry
Out-File -FilePath C:\Windows\System32\config\systemprofile\AppData\Roaming\NetFoundry\deviceKey.pem -InputObject $deviceKeyPem -Encoding ascii
Out-File -FilePath C:\Windows\System32\config\systemprofile\AppData\Roaming\NetFoundry\userKey.pem -InputObject $userKeyPem -Encoding ascii
#cleanup
$certStore = Get-Item Cert:\LocalMachine\REQUEST
$certStore.open('ReadWrite')
$certStore.remove($deviceCertificate)
$certStore.remove($userCertificate)
}
#Function adapted from CloudLAPS
function Get-AzureADDeviceCert {
Process {
# Define Cloud Domain Join information registry path
$AzureADJoinInfoRegistryKeyPath = "HKLM:\SYSTEM\CurrentControlSet\Control\CloudDomainJoin\JoinInfo"
# Retrieve the child key name that is the thumbprint of the machine certificate containing the device identifier guid
$AzureADJoinInfoKey = Get-ChildItem -Path $AzureADJoinInfoRegistryKeyPath | Select-Object -ExpandProperty "PSChildName"
if ($AzureADJoinInfoKey -ne $null) {
# Match key data against GUID regex
if ([guid]::TryParse($AzureADJoinInfoKey, $([ref][guid]::Empty))) {
$AzureADJoinCertificate = Get-ChildItem -Path "Cert:\LocalMachine\My" -Recurse | Where-Object { $PSItem.Subject -like "CN=$($AzureADJoinInfoKey)" }
}
else {
$AzureADJoinCertificate = Get-ChildItem -Path "Cert:\LocalMachine\My" -Recurse | Where-Object { $PSItem.Thumbprint -eq $AzureADJoinInfoKey }
}
# Retrieve the machine certificate based on thumbprint from registry key
if ($AzureADJoinCertificate -ne $null) {
# Determine the device identifier from the subject name
$AzureADDeviceID = ($AzureADJoinCertificate | Select-Object -ExpandProperty "Subject") -replace "CN=", ""
# Write event log entry with DeviceId
#Write-EventLog -LogName $EventLogName -Source $EventLogSource -EntryType Information -EventId 51 -Message "CloudLAPS: Found Cert for Device with Azure AD device identifier: $($AzureADDeviceID)"
# Handle return value
return $AzureADJoinCertificate
}
}
}
}
#Function adapted from CloudLAPS
function New-RSACertificateSignature {
<#
.SYNOPSIS
Creates a new signature based on content passed as parameter input using the private key of a certificate, to sign the computed hash of the content.
.DESCRIPTION
Creates a new signature based on content passed as parameter input using the private key of a certificate also passed as a parameter, to sign the computed hash of the content.
The certificate used must be available in the LocalMachine\My certificate store, and must also contain a private key.
.PARAMETER Content
Specify the content string to be signed.
.PARAMETER Thumbprint
Specify the thumbprint of the certificate.
.NOTES
Author: Nickolaj Andersen / Thomas Kurth
Contact: @NickolajA
Created: 2021-06-03
Updated: 2021-06-03
Version history:
1.0.0 - (2021-06-03) Function created
Credits to Thomas Kurth for sharing his original C# code.
#>
param(
[parameter(Mandatory = $true, HelpMessage = "Specify the content string to be signed.")]
[ValidateNotNullOrEmpty()]
[string]$Content,
[parameter(Mandatory = $true, HelpMessage = "Specify the certificate used to sign the content.")]
[ValidateNotNullOrEmpty()]
[system.security.cryptography.x509certificates.x509certificate2]$Certificate
)
Process {
#check cert exists and private key is available
if ($Certificate -ne $null) {
if ($Certificate.HasPrivateKey -eq $true) {
# Read the RSA private key
$RSAPrivateKey = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($Certificate)
if ($RSAPrivateKey -ne $null) {
if ($RSAPrivateKey -is [System.Security.Cryptography.RSACng]) {
# Construct a new SHA256Managed object to be used when computing the hash
$SHA256Managed = New-Object -TypeName "System.Security.Cryptography.SHA256Managed"
# Construct new UTF8 unicode encoding object
$UnicodeEncoding = [System.Text.UnicodeEncoding]::UTF8
# Convert content to byte array
[byte[]]$EncodedContentData = $UnicodeEncoding.GetBytes($Content)
# Compute the hash
[byte[]]$ComputedHash = $SHA256Managed.ComputeHash($EncodedContentData)
# Create signed signature with computed hash
[byte[]]$SignatureSigned = $RSAPrivateKey.SignHash($ComputedHash, [System.Security.Cryptography.HashAlgorithmName]::SHA256, [System.Security.Cryptography.RSASignaturePadding]::Pkcs1)
# Convert signature to Base64 string
$SignatureString = [System.Convert]::ToBase64String($SignatureSigned)
# Handle return value
return $SignatureString
}
}
}
}
}
}
#installs Ziti
Start-Process -FilePath .\Ziti.Desktop.Edge.Client -ArgumentList "/exenoui /qn /norestart" -wait
#defines the body of our HTTP request.
$Body = [ordered]@{
DeviceName = ""
DeviceEntraCert = ""
UserCSR = ""
DeviceCSR = ""
UserCSRSig = ""
DeviceCSRSig = ""
}
#get the device name
$body["DeviceName"] = $env:COMPUTERNAME
#get the device provisioning cert and load into data array
$DeviceEntraCert = Get-AzureADDeviceCert
$DeviceEntraCertBase64 = [System.Convert]::ToBase64String($DeviceEntraCert.RawData, [System.Base64FormattingOptions]::InsertLineBreaks)
$DeviceEntraCertPEM = @"
-----BEGIN CERTIFICATE-----
$DeviceEntraCertBase64
-----END CERTIFICATE-----
"@
$Body["DeviceEntraCert"] = $DeviceEntraCertPEM
$url = "<redacted>"
#load in signing requests
generate_keys $body
#sign the CSRs using the machine cert, to link the public keys generated to the device identity.
$Body["UserCSRSig"] = New-RSACertificateSignature -Content $body["UserCSR"] -certificate $DeviceEntraCert
$Body["DeviceCSRSig"] = New-RSACertificateSignature -Content $body["DeviceCSR"] -certificate $DeviceEntraCert
$response = Invoke-WebRequest -method "Post" -uri $url -Body ($Body | ConvertTo-Json) -ErrorVariable RespErr -ContentType 'application/json' -TimeoutSec 30 -UseBasicParsing
$json = $response.Content | ConvertFrom-Json
Out-File -FilePath C:\Windows\System32\config\systemprofile\AppData\Roaming\NetFoundry\User.CRT -InputObject ($json.userCert) -Encoding ascii
Out-File -FilePath C:\Windows\System32\config\systemprofile\AppData\Roaming\NetFoundry\Device.CRT -InputObject ($json.deviceCert) -Encoding ascii
&'C:\Program Files (x86)\NetFoundry Inc\Ziti Desktop Edge\ziti-edge-tunnel.exe' add -j "$PWD\Device.jwt" -c C:\Windows\System32\config\systemprofile\AppData\Roaming\NetFoundry\Device.CRT -k C:\Windows\System32\config\systemprofile\AppData\Roaming\NetFoundry\deviceKey.pem -i device-tunnel
&'C:\Program Files (x86)\NetFoundry Inc\Ziti Desktop Edge\ziti-edge-tunnel.exe' add -j "$PWD\User.jwt" -c C:\Windows\System32\config\systemprofile\AppData\Roaming\NetFoundry\User.CRT -k C:\Windows\System32\config\systemprofile\AppData\Roaming\NetFoundry\userKey.pem -i user-tunnel