Skip to content

fix(openvpn): derive the tls-auth HMAC digest from auth instead of hard-coding SHA-1 - #3189

Merged
wwqgtxx merged 1 commit into
MetaCubeX:Alphafrom
TokenPLS:fix/openvpn-tls-auth-digest
Sep 8, 2026
Merged

fix(openvpn): derive the tls-auth HMAC digest from auth instead of hard-coding SHA-1#3189
wwqgtxx merged 1 commit into
MetaCubeX:Alphafrom
TokenPLS:fix/openvpn-tls-auth-digest

Conversation

@TokenPLS

@TokenPLS TokenPLS commented Sep 7, 2026

Copy link
Copy Markdown

Summary

--tls-auth wrapped every control-channel packet with HMAC-SHA1 no matter what auth was configured. OpenVPN derives the tls-auth digest from --auth, so against a server running auth SHA256 or auth SHA512 the client's very first packet carries a 20-byte tag where the server expects 32 or 64 bytes. The server drops it with TLS Error: cannot locate HMAC in incoming packet, and the client times out with make OpenVPN handshake: read hard reset response after 4 retransmits: context deadline exceeded. Servers on OpenVPN's default auth SHA1 were unaffected, which is why tls-auth appeared to work.

Fixes #2992 (tls-auth + auth SHA512 never completes the handshake). Refs #2846.

Root cause

OpenVPN, at the pinned 2.7.5 source:

mihomo, before this change: transport/openvpn/tlsauth.go hard-coded sha1.Size for both the tag and the key material and hmac.New(sha1.New, key), and NewClient never passed config.Auth to it.

Change

  • NewTLSAuth(staticKey, keyDirection, authName): the digest comes from newDataChannelAuth(normalizeAuth(authName)) (MD5 / SHA1 / SHA256 / SHA384 / SHA512, the same set auth accepts). The tag is digest-sized and the HMAC key is the first digest-size bytes of the HMAC half (offset 64) of the direction's key slot — the three places OpenVPN reads --auth. An empty auth follows the config default (SHA256), as the data channel does. Unsupported digests are refused at construction.
  • NewClient passes config.Auth.
  • The simulated server in TestRealTLSRekeySurvivesExtendedAuthPending now wraps with the client's configured digest; it passed before only because both sides shared the same hard-coded SHA-1.
  • TLSAuthTagSize is replaced by (*TLSAuth).TagSize(), since the size is no longer a constant.

Tests

  • TestTLSAuthDigestFollowsAuth: for each of the five digests, a client/server round trip, the tag length equals the digest size, and the tag is HMAC-<digest> over packet id, header and payload keyed with the slot's HMAC material — independently derived from the OpenVPN sources above.
  • TestTLSAuthDigestMismatchIsRejected: a SHA256 tag does not verify under a SHA1 peer.
  • TestTLSAuthDefaultAndUnsupportedDigest: empty auth → SHA256; SHA224 refused.
  • Existing tls-auth, packet and rekey tests updated for the new signature.

go test ./transport/openvpn/ and go test -race ./transport/openvpn/ pass; go vet clean.

How this was found

We (Hako, a mihomo-based Apple client) were standing up an interop gate against an official OpenVPN 2.7.5 server for a user report about a tls-crypt profile (TokenPLS/Hako-Client#16). Adding a tls-auth arm with auth SHA256 exposed that the digest was fixed to SHA-1 — the same failure #2992 reports with SHA512. A run of that gate against the reference server will follow once available; the unit tests above are what this PR carries.

Reproduce

Server (openvpn --genkey tls-auth ta.key):

tls-auth ta.key 0
auth SHA256
verb 4

Client:

- name: ovpn
  type: openvpn
  server: <host>
  port: 1194
  proto: udp
  auth: SHA256
  key-direction: "1"
  ca: |
    ...
  cert: |
    ...
  key: |
    ...
  tls-auth: |
    -----BEGIN OpenVPN Static key V1-----
    ...
    -----END OpenVPN Static key V1-----

Before: server logs TLS Error: cannot locate HMAC in incoming packet from [AF_INET]..., client never completes the handshake. After: the handshake completes; a wrong key is still refused by the server with TLS Error: incoming packet authentication failed.

…rd-coding SHA-1

--tls-auth wrapped every control packet with HMAC-SHA1 regardless of the
configured auth, while OpenVPN derives the tls-auth digest from --auth
(init.c: tls_auth_key_type.digest = options->authname; crypto_openssl.c
hmac_ctx_init: key_len = EVP_MD_size(kt)). Against a server running
"auth SHA256" or "auth SHA512" the client's first packet carries a 20-byte
tag where the server expects 32 or 64 bytes, so the server drops it with
"TLS Error: cannot locate HMAC in incoming packet" and the handshake times
out after the hard-reset retransmits (MetaCubeX#2992). Servers on the OpenVPN default
"auth SHA1" were unaffected, which is why the wrapper appeared to work.

NewTLSAuth now takes the auth name and, through newDataChannelAuth, uses that
digest for the HMAC, sizes the tag to the digest, and takes digest-size bytes
of HMAC key material from offset 64 of the direction's key slot - the same
three places OpenVPN reads --auth. An empty auth follows the config default
(SHA256), like the data channel. The simulated server in the rekey test now
wraps with the client's configured digest; it used to pass only because both
sides shared the same hard-coded SHA-1.

Fixes MetaCubeX#2992
Refs MetaCubeX#2846
@wwqgtxx
wwqgtxx merged commit 6d179a1 into MetaCubeX:Alpha Sep 8, 2026
135 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants