cryptoutil

package
v0.9.5 Latest Latest
Warning

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

Go to latest
Published: Aug 14, 2026 License: MIT Imports: 11 Imported by: 0

README

internal/cryptoutil

Protocol-agnostic cryptographic primitives shared by every VPN transport in the tree: Diffie-Hellman groups, keyed PRFs and prf+ expansion, integrity transforms, and the two cipher shapes a VPN needs — a handshake cipher (SKCipher, whole-message seal/open) and a data-path cipher (ESPCrypter, per-packet, allocation-conscious).

Nothing here knows about IKEv2 or any IANA transform-ID registry. Mapping a negotiated transform ID onto one of these primitives is a protocol package's job (for IKEv2, internal/ikev2/transform), which keeps this layer reusable by WireGuard, OpenVPN, Nebula, and the rest.

Specifications

Primitive Reference
MODP-2048 Diffie-Hellman (group 14) RFC 3526
Curve25519 / ECDH RFC 7748
HMAC RFC 2104
prf+ key expansion RFC 7296 §2.13
AES-GCM AEAD RFC 5116, RFC 4106
AES-CBC + HMAC RFC 3602
ChaCha20-Poly1305 / XChaCha20 RFC 8439
BLAKE2s (hash + keyed MAC) RFC 7693

How the primitives compose

The pieces here are the links of a key schedule; a protocol chains them from a raw DH exchange down to the two ciphers that actually move packets.

flowchart TD
    DH["DHGroup<br/>(MODP-2048 · Curve25519)"] -->|shared secret| PRF
    PRF["PRF<br/>(HMAC-SHA-256/1)"] -->|SKEYSEED / master key| PLUS["prf+ expansion"]
    PLUS -->|key material| SK["SKCipher<br/>handshake: Seal/Open a whole message"]
    PLUS -->|key material| ESP["ESPCrypter<br/>data path: per-packet, append into caller buffer"]
    PLUS -->|integ key| INTEG["Integrity<br/>HMAC ICV (AES-CBC suites)"]
    ESP -. AES-CBC path uses .-> INTEG

API surface

  • Diffie-HellmanDHGroup interface; NewMODP2048() (group 14), NewECDH(curve, stripPointPrefix) (Curve25519 and the NIST curves; the strip flag drops the 0x04 uncompressed-point prefix that IKEv2 wire format omits).
  • PRF / expansionPRF via NewHMACPRF(newHash); carries the prf+ expansion used to stretch a seed into arbitrary key material.
  • IntegrityIntegrity via NewHMACIntegrity(newHash, keyLen, icvLen); the truncated-HMAC ICV for the AES-CBC AEAD-by-composition suites.
  • Handshake cipherSKCipher (NewAESGCMSKCipher, NewChaCha20Poly1305SKCipher, NewAESCBCSKCipher), sealing/opening a complete IKE SK payload. Rebuilds its AEAD per call — fine for the handshake, wrong for the data path (see caveats). AES-GCM and ChaCha20-Poly1305 (RFC 7634) share one generic AEAD implementation: identical framing (4-octet salt, 8-octet IV, 16-octet tag), differing only in the AEAD constructor and key length.
  • Data-path cipherESPCrypter (NewAESGCMESPCrypter, NewChaCha20Poly1305ESPCrypter, NewAESCBCESPCrypter): constructs its keyed AEAD once, then seals/opens by appending into a caller-supplied buffer.
  • AEAD constructors + hashesNewChaCha20Poly1305, NewXChaCha20Poly1305, NewBLAKE2s, NewBLAKE2s128MAC, NewBLAKE2s256MAC (for WireGuard/Nebula Noise).
  • Constant-time compareSecretEqual.

Implementation notes & caveats

  • SKCipher vs ESPCrypter is the central distinction. SKCipher rebuilds aes.NewCipher/cipher.NewGCM on every call — negligible a few times per handshake, ruinous per packet. ESPCrypter prepares the keyed AEAD once and is the type the data plane must use. Never seal packets with an SKCipher.
  • ESPCrypter is single-goroutine per direction. It is designed to be driven by one goroutine per SA direction (matching the pump). It is not safe for concurrent seals on one direction.
  • stripPointPrefix exists because IKEv2's KE payload carries a raw coordinate while Go's crypto/ecdh wants the 0x04-prefixed uncompressed form; the flag reconciles the two so the same DHGroup serves both wire formats.
  • MODP-2048 is ~70× slower than Curve25519 for a shared-secret computation (~3.9 ms vs ~53 µs — see the root README.md benchmarks). It exists for interop; curves are preferred wherever a peer allows.
  • Protocol-specific transform-ID → primitive mapping lives outside this package on purpose; do not add IANA registry knowledge here.

Documentation

Overview

Package cryptoutil implements the cryptographic primitives a VPN transport needs: Diffie-Hellman groups, keyed PRFs and prf+ expansion, integrity transforms, and the handshake (SKCipher) and data-path (ESPCrypter) ciphers.

It is deliberately protocol-agnostic: nothing here knows about IKEv2 or its IANA transform-ID registry. Mapping a negotiated transform ID onto one of these primitives is the job of a protocol package (for IKEv2, that is internal/ikev2/transform), which keeps this layer reusable by any protocol.

Index

Constants

View Source
const BLAKE2s128Size = 16

BLAKE2s128Size is the BLAKE2s-128 digest length in octets.

View Source
const BLAKE2sSize = blake2s.Size

BLAKE2sSize is the BLAKE2s-256 digest length in octets.

View Source
const ChaCha20Poly1305KeySize = chacha20poly1305.KeySize

ChaCha20Poly1305KeySize is the key length in octets for both the AEAD constructors below.

Variables

This section is empty.

Functions

func NewBLAKE2s

func NewBLAKE2s() hash.Hash

NewBLAKE2s returns an unkeyed BLAKE2s-256 hash (RFC 7693).

func NewBLAKE2s128MAC

func NewBLAKE2s128MAC(key []byte) (hash.Hash, error)

NewBLAKE2s128MAC returns a keyed BLAKE2s-128 hash. This is the 128-bit digest WireGuard calls MAC(): mac1, mac2 and the cookie are all this width, not the 256-bit one.

func NewBLAKE2s256MAC

func NewBLAKE2s256MAC(key []byte) (hash.Hash, error)

NewBLAKE2s256MAC returns a keyed BLAKE2s-256 hash — BLAKE2's native MAC mode, which needs no HMAC construction around it. The key must be at most 32 octets.

func NewChaCha20Poly1305

func NewChaCha20Poly1305(key []byte) (cipher.AEAD, error)

NewChaCha20Poly1305 returns the AEAD (RFC 8439) keyed by a 32-octet key, with a 12-octet nonce. This is WireGuard's transport and handshake cipher.

func NewXChaCha20Poly1305

func NewXChaCha20Poly1305(key []byte) (cipher.AEAD, error)

NewXChaCha20Poly1305 returns the extended-nonce variant (24-octet nonce), keyed by a 32-octet key. WireGuard uses it for cookie replies, where the nonce is random rather than a counter and so needs the larger space.

func SecretEqual added in v0.4.0

func SecretEqual(a, b []byte) bool

SecretEqual reports whether two secret-derived values are equal, in time that does not depend on how much of them matches.

Use it for anything an attacker can supply and retry: authentication responses, MACs, tags, proofs. Length is not secret — a mismatch there is reported immediately, since the sizes involved are fixed by the protocol and visible on the wire anyway.

Types

type DHGroup

type DHGroup interface {
	// Generate returns the public key bytes (wire form) for a fresh private key.
	Generate() (pub []byte, err error)
	// ComputeSecret takes the peer public key bytes and returns the shared secret.
	ComputeSecret(peerPub []byte) ([]byte, error)
}

DHGroup abstracts a Diffie-Hellman group: generate an ephemeral key, expose the public value in wire form, and compute a shared secret.

func NewECDH

func NewECDH(curve ecdh.Curve, stripPointPrefix bool) DHGroup

NewECDH returns an ECDH group over curve. When stripPointPrefix is set, the uncompressed-point marker (0x04) that crypto/ecdh puts in front of an X||Y public value is removed on the wire and re-added on parse: the NIST curves transmit bare X||Y (RFC 5903), whereas X25519 has no such prefix.

func NewMODP2048

func NewMODP2048() DHGroup

NewMODP2048 returns the 2048-bit MODP group (RFC 3526).

type ESPCrypter

type ESPCrypter interface {
	// Overhead returns the number of octets Seal adds beyond the plaintext
	// (IV + ICV for AEAD; IV + MAC for CBC-ETM). Callers use it to size buffers.
	Overhead() int
	// BlockLen is the cipher block size for ESP trailer padding (1 for AEAD).
	BlockLen() int
	// Seal appends iv||ciphertext||icv for plaintext (authenticating aad) to
	// dst and returns the extended slice. aad is not encrypted.
	Seal(dst, aad, plaintext []byte) ([]byte, error)
	// Open verifies and decrypts ivCtIcv (authenticating aad), appending the
	// recovered plaintext to dst and returning the extended slice.
	Open(dst, aad, ivCtIcv []byte) ([]byte, error)
}

ESPCrypter is an allocation-conscious cipher for the ESP data path. Unlike SKCipher (which rebuilds its cipher state on every call for handshake use), an ESPCrypter prepares its keyed cipher once and then seals/opens packets appending into a caller-supplied buffer, so the per-packet hot path performs no cipher construction and minimal allocation.

A single ESPCrypter is intended to be driven by one goroutine at a time (one per SA direction), matching the userspace data-plane pump, which uses separate crypters for the inbound and outbound directions. It is not safe for concurrent use.

func NewAESCBCESPCrypter

func NewAESCBCESPCrypter(keyBits int, encKey []byte, integ *Integrity, integKey []byte) (ESPCrypter, error)

NewAESCBCESPCrypter builds a prepared AES-CBC + HMAC (encrypt-then-MAC) ESP crypter. keyBits is the AES key length (0 selects AES-256); integ and integKey supply the MAC.

func NewAESGCMESPCrypter

func NewAESGCMESPCrypter(keyBits int, encKey []byte) (ESPCrypter, error)

NewAESGCMESPCrypter builds a prepared AES-GCM-16 ESP crypter. keyBits is the AES key length (0 selects AES-256); encKey is the ESP encryption key followed by its 4-octet GCM salt (RFC 4106).

func NewChaCha20Poly1305ESPCrypter added in v0.6.0

func NewChaCha20Poly1305ESPCrypter(encKey []byte) (ESPCrypter, error)

NewChaCha20Poly1305ESPCrypter builds a prepared ChaCha20-Poly1305 ESP crypter (RFC 7634). encKey is the 32-octet key followed by its 4-octet salt; the wire framing (8-octet IV, 16-octet tag, salt||IV nonce) matches AES-GCM-16, so it reuses the same prepared-AEAD crypter.

type Integrity

type Integrity struct {
	KeyLen int // key length in bytes
	ICVLen int // truncated output length in bytes
	// contains filtered or unexported fields
}

Integrity is a MAC transform used by non-AEAD SK ciphers (RFC 7296 2.14).

func NewHMACIntegrity

func NewHMACIntegrity(newHash func() hash.Hash, keyLen, icvLen int) *Integrity

NewHMACIntegrity builds an HMAC integrity transform over the given hash, truncating its output to icvLen octets. keyLen is the key length in octets.

func (*Integrity) Sum

func (i *Integrity) Sum(key, data []byte) []byte

Sum computes the truncated MAC over data.

type PRF

type PRF struct {
	Size int // output length in bytes
	// PreferredKeyLen is the key length used when the PRF key is variable and
	// we are choosing one (equal to the output size for HMAC PRFs).
	PreferredKeyLen int
	// contains filtered or unexported fields
}

PRF is a keyed pseudorandom function (RFC 7296 section 2.13).

func NewHMACPRF

func NewHMACPRF(newHash func() hash.Hash) *PRF

NewHMACPRF builds an HMAC-based PRF over the given hash. For HMAC PRFs the preferred key length equals the hash output size (RFC 7296 section 2.13).

func (*PRF) Apply

func (p *PRF) Apply(key, data []byte) []byte

Apply computes prf(key, data).

func (*PRF) Plus

func (p *PRF) Plus(key, seed []byte, n int) []byte

Plus implements prf+ (RFC 7296 section 2.13):

prf+(K,S) = T1 | T2 | T3 | ...
T1 = prf(K, S | 0x01)
Tn = prf(K, Tn-1 | S | n)

It returns exactly n bytes.

type SKCipher

type SKCipher interface {
	// KeyLen is the encryption key length in bytes.
	KeyLen() int
	// IVLen is the length of the per-message IV/nonce written on the wire.
	IVLen() int
	// ICVLen is the length of the integrity tag/MAC appended after ciphertext.
	ICVLen() int
	// BlockLen is the cipher block size for padding; 1 for stream/AEAD.
	BlockLen() int
	// AEAD reports whether integrity is provided by the cipher itself.
	AEAD() bool

	// Seal encrypts plaintext and returns iv||ciphertext||icv. aad is the
	// authenticated-but-not-encrypted prefix (IKE header .. SK header).
	Seal(encKey, integKey, aad, plaintext []byte) ([]byte, error)
	// Open verifies and decrypts. For non-AEAD, integ is checked over
	// aad||iv||ciphertext before decryption (encrypt-then-MAC).
	Open(encKey, integKey, aad, ivCtIcv []byte) (plaintext []byte, err error)
}

SKCipher protects the encrypted (SK) payload. It handles both AEAD ciphers (AES-GCM, ChaCha20-Poly1305) and the classic encrypt-then-MAC construction (AES-CBC + HMAC). The interface abstracts over both so the message layer is agnostic to which suite was negotiated.

Layout of the SK payload body (RFC 7296 section 3.14):

[ IV | ciphertext(padded) | ICV ]

For AEAD, IV is the explicit nonce portion and ICV is the auth tag. The AAD covers the IKE header and all preceding payload headers up to and including the SK generic header.

func NewAESCBCSKCipher

func NewAESCBCSKCipher(keyBits int) (SKCipher, error)

NewAESCBCSKCipher returns an AES-CBC SK cipher for the given key length in bits; 0 selects AES-256. Integrity is supplied separately by an Integrity transform, and callers must route Seal/Open through SealETM/OpenETM.

func NewAESGCMSKCipher

func NewAESGCMSKCipher(keyBits int) (SKCipher, error)

NewAESGCMSKCipher returns an AES-GCM-16 SK cipher (RFC 5282) for the given key length in bits; 0 selects AES-256.

func NewChaCha20Poly1305SKCipher added in v0.6.0

func NewChaCha20Poly1305SKCipher() (SKCipher, error)

NewChaCha20Poly1305SKCipher returns a ChaCha20-Poly1305 SK cipher (RFC 7634). Its wire framing is identical to AES-GCM-16 — a 4-octet salt in the trailing key material, an 8-octet explicit IV and a 16-octet tag — so it shares the generic AEAD implementation; only the key is fixed at 256 bits and there is no key-length attribute to negotiate.

type SKParams

type SKParams struct {
	Cipher SKCipher
	Integ  *Integrity // nil for AEAD
}

SKParams bundles a negotiated cipher and (for non-AEAD) integrity transform.

Jump to

Keyboard shortcuts

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