keymethod2

package
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 8, 2026 License: LGPL-2.1, LGPL-2.1-or-later Imports: 9 Imported by: 0

Documentation

Overview

Package keymethod2 implements the OpenVPN key-method-2 auth exchange in both framings it is spoken in: the stock uint16_be one, and the AWS Client VPN patched uint32 one that exists because SAML assertions outgrow a uint16 length field.

It is bytes and nothing else — what the client advertises about itself, what it sends, and what it reads back — so every claim it makes can be checked against a known answer rather than against a live server. The tunnel options string and the peer-info block are here because they are part of this packet's payload, and a deployment told the wrong cipher rejects the credentials that came with it.

Index

Constants

View Source
const (
	// IVProtoDataV2 makes a 2.6 server assign a peer-id and speak
	// P_DATA_V2. 2.4 and 2.5 do the same for any IV_PROTO value of 2 or
	// more, whatever this bit says — see the root package's WithholdDataV2,
	// which is why that lever sends 0 rather than clearing a bit.
	IVProtoDataV2 = 1 << 1
	// IVProtoRequestPush says the client will send PUSH_REQUEST itself.
	IVProtoRequestPush = 1 << 2
	// IVProtoTLSKeyExport offers the RFC 5705 EKM data-key derivation.
	IVProtoTLSKeyExport = 1 << 3
	// IVProtoAuthPending is upstream's IV_PROTO_AUTH_PENDING_KW, "Supports
	// signaling keywords with AUTH_PENDING, e.g. timeout=xy" (ssl.h:89-90).
	// All the bit decides is the *form* of a server's AUTH_PENDING message:
	// push.c:457-470 appends ",timeout N" when it is claimed and sends a bare
	// AUTH_PENDING when it is not. It is not the AUTH_FAILED,CRV1 challenge,
	// which push.c:119 handles on a different message and gates on no IV_PROTO
	// bit at all, and it is not SAML. The mid-session AUTH_PENDING loop
	// itself is only partially implemented: the bit is advertised and the
	// message is recognised, and nothing resumes the exchange it asks for.
	IVProtoAuthPending = 1 << 4

	// IVProtoImplemented is every bit above — 30 — and is what this client
	// advertises unless a caller withholds DATA_V2 for the P_DATA_V1 matrix
	// isolate. Nothing may be added to it that is not implemented; see
	// PeerInfo.
	IVProtoImplemented = IVProtoDataV2 | IVProtoRequestPush | IVProtoTLSKeyExport | IVProtoAuthPending

	// IVProtoNoExtensions claims none of them, which is what the root
	// package's WithholdDataV2 sends and the only value that makes every
	// pinned server version decline to assign a peer-id.
	IVProtoNoExtensions = 0
)

The IV_PROTO bits this client implements, and their sum.

They are named rather than added up by hand because the sum is asked for twice — once whole, once without DATA_V2 — and a literal 30 in a string is not something a reader can check against the list of bits.

Reference: OpenVPN 2 src/openvpn/ssl.h IV_PROTO_* and doc/openvpn-protocol-extensions.txt.

Variables

This section is empty.

Functions

func ConsumeServerAuth

func ConsumeServerAuth(r io.Reader) (ks prf.KeySource, serverOpts string, err error)

ConsumeServerAuth reads the server's key-method-2 auth packet, which arrives over TLS immediately after the handshake, and returns the server's key material and its own options string.

The key source is the point of parsing rather than draining: those 64 bytes are half the input to the classic key derivation. The options string is the other point — it carries the deployment's link-mtu, cipher, auth and key-method, and is the best available fingerprint of what a server runs.

Stock OpenVPN CE framing:

[0x00 0x00 0x00 0x00]         literal zero prefix
[0x02]                        key_method byte
[random1 32B][random2 32B]    64 bytes server TLSPRF
[uint16_be(len+1)][options\0][username\0-block][password\0-block][peer_info\0-block]

Some AWS deployments use the large-token patched format, which prefixes the packet with its total length as uint32_le and widens the string prefixes to uint32_be. Both are tolerated and auto-detected from the 4-byte header: a plausible LE total length means AWS framing, anything else is the stock zero prefix.

Every field is read for its declared length rather than drained into a scratch buffer, so the stream is left on the first byte of the next control message. A truncated packet is an error, which every caller maps to diag.ClassProtocol at diag.StageAuth; the options string is returned alongside it where it was read successfully, so a report keeps the fingerprint of a server that misframes something after it.

func PeerInfo

func PeerInfo(ivProto uint32) string

PeerInfo renders the IV_* capability advertisement sent in the auth packet, for one IV_PROTO value. It is the only place the block's shape exists.

IV_PROTO must advertise ONLY features this client implements: a server enables a negotiated feature when the client claims it, and then speaks it on the wire. CC_EXIT_NOTIFY (1<<7), AUTH_FAIL_TEMP (1<<8), DYN_TLS_CRYPT (1<<9), DATA_EPOCH (1<<10), DNS_OPTION_V2 (1<<11) and PUSH_UPDATE (1<<12) are therefore withheld. DATA_EPOCH in particular selects a data-channel packet format and key schedule this client cannot decode, and control-channel TLS and SAML would still succeed while every data packet failed.

IVProtoImplemented advertises the four bits named above — 30 — which keeps EKM (correct key derivation) while making the server fall back to the classic AEAD data channel. The AWS CRV1 challenge this client answers at connect rides on none of these bits.

IV_CIPHERS is generated from the cipher table in internal/crypto rather than written out here: a server speaks whatever cipher it selects from that list, so advertising one we cannot build is a promise broken at the first data packet. TestIVCiphersIsHonest asserts the two still agree.

func SendAuth

func SendAuth(w io.Writer, params TunnelParams, username, password string, framing Framing, ivProto uint32) (prf.KeySource, error)

SendAuth sends the OpenVPN key-method-2 auth packet over the TLS connection immediately after the TLS handshake completes. ivProto is the value the peer-info block advertises; see PeerInfo, and see Framing for which of the two wire formats below applies.

Wire format — FramingAWSLargeToken (key_method_2_write patch in AWS ssl.c):

[total_len uint32_le]         first 4 bytes = total packet length (LE)
[0x02]                        key_method byte
[pre_master 48B][random1 32B][random2 32B]   112 bytes client TLSPRF
[uint32_be(len+1)][options\0]
[uint32_be(len+1)][username\0]
[uint32_be(len+1)][password\0]
[uint32_be(len+1)][peer_info\0]

Wire format — FramingStock:

[0x00 0x00 0x00 0x00]         literal zero prefix (key_method_2 legacy header)
[0x02]                        key_method byte
[pre_master 48B][random1 32B][random2 32B]   112 bytes client TLSPRF
[uint16_be(len+1)][options\0]
[uint16_be(len+1)][username\0]
[uint16_be(len+1)][password\0]
[uint16_be(len+1)][peer_info\0]

func TunnelOptions

func TunnelOptions(p TunnelParams) string

TunnelOptions returns the options string sent in the key-method-2 auth packet. The string is dynamic because proto and link-mtu differ between TCP and UDP connections — advertising the wrong proto causes AUTH_FAILED.

Values match what openvpn3-core 3.11.6 sends for each transport at a 1500-byte TUN MTU. link-mtu tracks a configured TUN MTU by retaining the same transport overhead: 21 bytes for UDP and 43 bytes for TCP.

The cipher, auth and keysize fields describe what this connection will actually use, because the server checks the string against its own configuration: a CBC deployment told AES-256-GCM is a plausible cause of a rejection that looks like a credential problem. An AEAD cipher reports its digest as [null-digest], because it authenticates its own output; a CBC cipher names the real digest. keysize is the cipher's key length in bits.

Types

type Framing

type Framing int

Framing selects the wire format of the key-method-2 auth packet. It is a property of the peer being authenticated to, not of the client, so it belongs to whatever decides how to authenticate. A peer that is sent the packet in the other shape answers AUTH_FAILED.

const (
	// FramingStock is stock OpenVPN's format: a zero prefix and uint16_be
	// string lengths. It is the zero value because it is what an unpatched
	// server speaks, which is every server but one kind.
	FramingStock Framing = iota
	// FramingAWSLargeToken is AWS Client VPN's patched format: a uint32_le
	// total length followed by uint32_be string lengths. A SAML assertion
	// does not fit the uint16 prefix stock OpenVPN uses, which is why the
	// patch exists.
	FramingAWSLargeToken
)

func (Framing) String

func (f Framing) String() string

String names the framing for a log line or a report.

type TunnelParams

type TunnelParams struct {
	// Proto is the transport actually dialled, which is not necessarily the
	// profile's: link-mtu and the proto field both follow it.
	Proto profile.Proto
	// TunMTU is the configured TUN MTU; zero means OpenVPN's 1500 default.
	TunMTU int
	// CipherName is the negotiated cipher's OpenVPN name.
	CipherName string
	// AuthName is the negotiated CBC digest's name. Empty for an AEAD
	// cipher, which reports its digest as [null-digest] because it
	// authenticates its own output.
	AuthName string
	// KeySizeBits is the cipher's key length in bits.
	KeySizeBits int
}

TunnelParams is what the options string advertises about the data channel. It is plain values rather than the negotiated datachannel.Params it derives from, so this package depends on the wire format alone and not on the cipher table the caller resolved against.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL