wireguard

package module
v0.1.1 Latest Latest
Warning

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

Go to latest
Published: Oct 10, 2026 License: BSD-3-Clause Imports: 14 Imported by: 0

README

go-authn/wireguard

WireGuard public keys for the people a go-authn provider knows. It does two things: it says what a public key is, and it carries the signed list of keys people have registered, which a gateway turns into peers.

WireGuard authenticates a peer by its static public key and by nothing else. The protocol has no certificates, no expiry and no revocation. The question may this key connect, and whose is it? therefore has to be answered outside it, and answered again whenever the answer changes. go-authn/bridge answers it:

device            bridge                               gateway (claimward)
  │ wg genkey (stays here)
  │── token, scope "wireguard" ──▶ POST /wireguard/key {public_key, device}
  │                                  key → person, for a lease;
  │                                  DisablePerson / DisableIdP take it back
  │                                                       │
  │                    GET /wireguard/peers  ◀────────────┤ its own client,
  │                    signed, typ wireguard-peers+jwt ──▶│ scope "wireguard_peers"
  │                                                       │ Source.Fetch → peers
  │                    SSF events (person disabled) ─────▶│ fetch again now

⛔ Only public keys pass through here. A private key is generated on the device and never leaves it, and nothing in this package has a place for one.

A key

ParseKey reads a key the way wg(8) prints it: standard base64, 44 characters with padding. It refuses two things:

  • Any other spelling of the same bytes. Strict decoding, and the text must print back exactly as it came. A key therefore has one text form, and a store keyed on the text cannot hold one key twice under two names.
  • A point of low order, the all-zero key among them. Every exchange with such a point gives the same shared secret whatever the other side's key (RFC 7748 §6.1), so it authenticates nobody, and WireGuard refuses the handshake. Registered, it would be a peer that cannot connect, holding an address. crypto/ecdh decides this by computing an exchange, so the top-bit variants are refused too, which a byte comparison would miss. The test checks the seven points libsodium lists, each with and without the top bit.

The list

A List is signed by the provider as a JWT whose typ is wireguard-peers+jwt (ListType). Neither an access token nor an ID token can pass for one, and a list cannot pass for a token. It is addressed to one gateway client and carries:

claim
version increases every time a key is taken back
iat, exp written, and stops being an answer; a gateway that cannot fetch a newer one by then has no list, rather than a stale one
peers public_key, sub, username, device, exp (the key's lease)

sub is the person's subject as the client that registered the key sees it, public or pairwise. It is therefore the same value a gateway finds in that person's access tokens.

Source is the gateway's side. NewSource reads the provider's configuration at startup. Fetch then:

  1. gets a token for the gateway itself (client_credentials, scope wireguard_peers, credentials sent as RFC 6749 §2.3.1 says);
  2. fetches the list;
  3. verifies it with go-authn/oidc: the signature against the provider's key set, the issuer, the audience (this gateway), the expiry and the typ.

It refuses:

why
a list older than one already seen an old list is signed too, and may list a key taken back since; within its lifetime its signature alone would pass
a key listed twice whichever a gateway kept, the other person's traffic would arrive under the wrong name
a peer with no subject it belongs to nobody
a key that is not one (see above)
an issuer or token endpoint in the clear the gateway's secret and the list would cross a link anybody can read; loopback is exempt

The judge

The keys ParseKey must accept are made by wg(8) itself (wireguard-tools), installed and required in the CI lanes that run the tests (WIREGUARD_REQUIRE_JUDGE=1), so a missing judge fails rather than skips. Elsewhere, pyca/cryptography's X25519 (OpenSSL) stands in. Coverage is 100%, and the CI floor is 100.

Licence

BSD-3-Clause.

Documentation

Overview

Package wireguard is what a go-authn provider and a WireGuard gateway agree on about people's keys: what a public key is, and the signed list of the keys people have registered, which the gateway turns into peers.

WireGuard authenticates a peer by its static public key and nothing else: there are no certificates, no expiry and no revocation in the protocol. So the question "may this key connect, and whose is it?" has to be answered outside it, and answered again whenever the answer changes. Here the provider (go-authn/bridge) answers it: a person registers the public key of each device with a token, for a lease, and the provider takes the keys back when the person or their institution is disabled. The gateway pulls the answer as a List, signed by the provider, with a Source.

⛔ Only PUBLIC keys pass through here. A private key is generated on the device and never leaves it; nothing in this package has a place for one.

Index

Constants

View Source
const (
	// ListType is the "typ" of a signed [List]: not "JWT", so that nothing
	// verifying tokens takes a list for one, nor the reverse.
	ListType = "wireguard-peers+jwt"
	// ListPath is where a provider serves the list, under its issuer.
	ListPath = "/wireguard/peers"
	// KeyPath is where a person's client registers and removes a key.
	KeyPath = "/wireguard/key"
	// ScopeKeys is the scope a person's token needs to register a key.
	ScopeKeys = "wireguard"
	// ScopePeers is the scope a gateway's own token needs to read the list.
	ScopePeers = "wireguard_peers"
)

The names a provider and a gateway share.

Variables

This section is empty.

Functions

This section is empty.

Types

type Key

type Key [32]byte

Key is a WireGuard public key: an X25519 public value, 32 bytes.

func ParseKey

func ParseKey(s string) (Key, error)

ParseKey reads a public key as wg(8) prints it: standard base64, 44 characters with its padding.

It refuses:

  • any other spelling of the same bytes. The decoding is strict, so a key has exactly one text form, and a list or a store keyed on the text cannot hold one key twice under two names;
  • a key of low order, the all-zero key among them. Every exchange with one gives the same shared secret whatever the other side's key (RFC 7748 6.1), so it authenticates nobody, and WireGuard itself refuses the handshake. Registered, it would be a peer that cannot connect, holding an address, and a name in the list for nothing.

func (Key) String

func (k Key) String() string

String is the key as wg(8) prints it.

type List

type List struct {
	// Version increases every time a key is taken back. A gateway that has
	// seen a version refuses an older one, so an old list -- signed, and
	// listing a key since revoked -- cannot be replayed to it.
	Version uint64
	// IssuedAt is when the provider wrote it, and Expires when it stops
	// being an answer: a gateway that cannot fetch a newer one by then has
	// no list rather than a stale one.
	IssuedAt, Expires time.Time
	Peers             []Peer
}

List is the keys a gateway may accept, at one moment.

func (List) Claims

func (l List) Claims(issuer, audience string) map[string]any

Claims are the claims of the JWT a provider signs for one gateway, the audience. The provider signs them with ListType as the "typ".

type Peer

type Peer struct {
	Key Key
	// Subject is the person's "sub" as the client that registered the key
	// sees it: the value a gateway also finds in that person's access
	// tokens, whether the client's subjects are public or pairwise.
	Subject string
	// Username is the name the provider calls them, for logs and routing
	// rules; Subject is what ties a key to a person.
	Username string
	// Device is the name the person gave the key, if any.
	Device string
	// Expires is the end of the key's lease. A gateway removes the peer then,
	// list or no list.
	Expires time.Time
}

Peer is one key a person registered, as a gateway needs it.

type Source

type Source struct {
	// contains filtered or unexported fields
}

A Source fetches a provider's list for one gateway. It is safe for concurrent use; build one at startup and keep it, since it remembers the newest version it has seen.

func NewSource

func NewSource(ctx context.Context, cfg SourceConfig) (*Source, error)

NewSource reads the provider's configuration, as a database is pinged at startup: a provider that is not answering is a gateway that cannot admit anybody, and it should say so before it starts.

func (*Source) Fetch

func (s *Source) Fetch(ctx context.Context) (*List, error)

Fetch asks for the list and verifies it: signed by the provider, of the list's own type, addressed to this gateway, unexpired, and no older than the newest this Source has taken.

type SourceConfig

type SourceConfig struct {
	// Issuer is the provider, as its tokens name it.
	Issuer string
	// ClientID and ClientSecret are the gateway's own client at the
	// provider: a confidential client the provider lets read the list.
	ClientID, ClientSecret string
	// Client is the HTTP client, for a proxy, a private CA or a timeout.
	Client *http.Client
	// Now overrides the clock, for tests.
	Now func() time.Time
}

SourceConfig is how a gateway reaches its provider.

Jump to

Keyboard shortcuts

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