HKU uses Cisco AnyConnect protocol and should use the Cisco software to connect the VPN. This post tries to use the openconnect to do the same thing in the command shell and make it all automatic.

OpenConnect is a cross-platform multi-protocol SSL VPN client which supports a number of VPN protocols, including Cisco AnyConnect, Juniper SSL, Palo Alto, etc. We can install openconnect on MacOS,

brew install openconnect

(I believe you know when it is Linux / Windows). Then we have to configure the connection. The basic usage is

openconnect <SERVER_NAME>

Well, simply doing so will not get you connected. A successful sample will be

sudo openconnect <VPN_SERVER>\
--protocol=anyconnect\
--useragent=AnyConnect\
--gnutls-priority "NORMAL:-VERS-ALL:+VERS-TLS1.2:+RSA:+AES-128-CBC:+SHA1"\
--servercert <SERVER_CERT>

Here we set the protocol as anyconnect to denote the Cisco AnyConnect protocol.

useragent should be specified since it will set the User-Agent string in the HTTP header, which is required by the HKU server.

To trust the server, you can specify servercert, which pins the server’s certificate using a specific SHA-256 hash. You can neglect it when logging in the first time, and the warning will give you such value.

Notably, HKU does not employ a standard encryption algorithm of the SSL connection. One should specify gnutls-priority to match the SSL/TLS configuration. Specifically, we enable TLS1.2 and specify the use of RSA for key exchange, AES-128-CBC for encryption and SHA-1 for hashing.

Here you should be able to connect to HKU VPN by inputting the username, password and OTP.

Yet it is still not satisfying. We still have to enter three times. Username and Password are easy to solve. Most importantly, we need to get rid of the phone to bypass the authentication.

We have to configure the OTP on our laptop. Previously we use Microsoft Authenticator and I recently find that HKU also allows other authenticator to pass the MFA.

We can login to My Sign-Ins and add sign-in method. You can choose non-Micorsoft Authenticator and you will get a QR code / secret key.

Now we need to choose an OTP client.

OTP is a password certification scheme. The key idea is the client and the certificate server shares a secret key and judge based on timestamps. The algorithm is to use HMAC to generate a hash value by combining the shared secret and timestamps.

Given that, if we have the authorised secret, we can generate the OTP on the client side every time. There are many software to do so, including the browser extension and ⌘ line tools (本地纯终端界面的 2FA TOTP 验证码生成器 - V2EX). Here we use 2fa as an example. We first install GoLang,

brew install go

and install the repo

go install rsc.io/2fa@latest

Do not forget to add go in the path:

export GOPATH="$HOME/go"
export PATH=$GOPATH/bin:$PATH

Then you should have 2fa in the environment. Enter

2fa -add <NAME>

You will be prompted to enter a key. Enter the key that you have acquired above (if it expires, you can refresh the page to get a new one), and you will get a 6-digit code. Enter the 6-digit code in the browser by clicking “Next”. If everything goes well, your client is acknowledged. Next time, you will only have to enter

2fa <NAME>

to get the code.

By now, we are getting rid of the Microsoft Authenticator and realize all the process in the ⌘ line. Now we will enter those pass keys in the order we are prompted. We use expect as the tools. You should install it first,

brew install expect

and create a new file connect-vpn.expect:

#!/usr/bin/expect -f
# Variables for VPN
set vpn_server "<SERVER_ADDRESS>"
set username "<USER_NAME>"
set password "<PASSWORD>"

# Function to fetch OTP
proc getOtp {} {
	set otp_command "2fa <NAME>"
	set otp [exec /bin/sh -c $otp_command]
	return $otp
}

spawn sudo openconnect $vpn_server --protocol=anyconnect -v --useragent=AnyConnect --servercert <SERVER_CERT> --gnutls-priority "NORMAL:-VERS-ALL:+VERS-TLS1.2:+RSA:+AES-128-CBC:+SHA1"
  

expect {
    "Username:" {
        send "$username\r"
        exp_continue
    }
    "Password:" {
        send "$password\r"
        exp_continue
    }
    "Response:" {
        set otp [getOtp]
        send "$otp\r"
    }
    timeout {
        puts "Timeout occurred, authentication might be stalled.\n"
        exit 1
    }
    eof {
        puts "Connection closed unexpectedly. Authentication might have failed.\n"
        exit 1
    }
}

interact

The interaction is realized by expect interactions. And the last interact hands back control once the login is complete. Here we use spawn to start a new process.

Now you can directly opening the VPN by

sudo ./connect-vpn.expect