tlsconf

package
v0.0.6 Latest Latest
Warning

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

Go to latest
Published: Oct 16, 2026 License: MIT Imports: 19 Imported by: 0

Documentation

Overview

Package tlsconf centralises cloop's transport-security decisions: the server-side TLS parameters for `cloop ui` / `cloop serve`, and the SPKI pinning primitives the executor agent uses to decide whether the TLS peer that answered DNS is really its control plane.

Why one package rather than a tls.Config per caller: the hub, the REST API server and the agent must agree on what "acceptable transport" means. When each site builds its own tls.Config, the weakest one becomes the deployment's real floor, and nothing fails loudly when they drift apart. Everything here is deliberately small and pure so tests/security can assert on it directly.

Pinning is SPKI-based, not certificate-based, on purpose. A pin over the Subject Public Key Info survives certificate renewal — the near-universal operational event — as long as the operator reuses the key, so a fleet of enrolled edge devices does not all break on the day a 90-day certificate rolls over. Pinning the whole certificate would turn every renewal into a fleet-wide re-enrollment. (This mirrors RFC 7469's choice for the same reason.)

Pinning here is *additive*: it runs through tls.Config.VerifyPeerCertificate, which the Go runtime only calls after the normal chain/hostname verification has already succeeded. This package never sets InsecureSkipVerify and no caller should: "the pin matches" must mean "a valid certificate that also matches the pin", never "an invalid certificate we decided to accept".

Index

Constants

View Source
const (
	KeyFileMode  os.FileMode = 0o600
	CertFileMode os.FileMode = 0o644
)

File modes for generated key material. The private key is 0600 because it is the server's identity; the certificate is 0644 because it is public by construction and agents/operators need to read it to compute a pin.

View Source
const DefaultSelfSignedTTL = 365 * 24 * time.Hour

DefaultSelfSignedTTL bounds a generated development certificate.

One year rather than ten: a self-signed certificate that outlives the experiment it was made for is how "temporary" dev TLS ends up terminating production traffic. An expiry inside the horizon of the person who ran the command is the cheapest available forcing function.

View Source
const PinPrefix = "sha256:"

PinPrefix is the algorithm tag every pin carries. It is spelled out rather than implied so a future second hash can be added without silently reinterpreting pins already deployed on edge devices.

Variables

View Source
var ErrEndpointInvalid = errors.New("tlsconf: invalid endpoint URL")

ErrEndpointInvalid is returned for URLs that are malformed or use a scheme this transport does not speak.

View Source
var ErrInsecureTransport = errors.New("tlsconf: refusing to send credentials over an unencrypted connection")

ErrInsecureTransport is returned when an endpoint would send credentials in the clear to a host that is not loopback.

View Source
var ErrNoPin = errors.New("tlsconf: malformed pin")

ErrNoPin is returned when a pin string cannot be parsed.

View Source
var ErrPinMismatch = errors.New("tlsconf: server public key does not match any configured pin")

ErrPinMismatch is returned by the VerifyPeerCertificate closure when the peer presented a valid certificate whose SPKI is not in the pin set. It is distinct from a generic handshake failure because the operator response is different: a mismatch means either the certificate was rotated to a new key (re-pin) or something is impersonating the control plane (investigate).

Functions

func CheckKeyPermissions

func CheckKeyPermissions(path string) string

CheckKeyPermissions reports a warning string when a private key is readable by anyone but its owner, or "" when the mode is acceptable. Windows has no comparable bits, so it is skipped there.

func CipherSuites

func CipherSuites() []uint16

CipherSuites returns the TLS 1.2 suites cloop enables.

The order is not a preference. Since Go 1.17 crypto/tls orders the suites itself, server-side, from the ones a config enables — which is why the PreferServerCipherSuites flag these configs used to set was ignored, and is gone since Task 20411 (TestServerChoosesTheSuiteFromItsOwnList pins the result: the server's choice, from this list only, whatever order a client offers).

Only AEAD suites with forward secrecy are listed: ECDHE key agreement plus AES-GCM or ChaCha20-Poly1305. Everything Go still supports for compatibility — CBC-mode suites (Lucky13, and a long tail of padding-oracle variants), static RSA key exchange (no forward secrecy, so one key compromise retroactively decrypts every recorded session) — is excluded.

TLS 1.3 suites are deliberately absent: Go does not allow configuring them, and all three are AEAD-with-forward-secrecy by design.

func ClientConfig

func ClientConfig(opts ClientOptions) (*tls.Config, error)

ClientConfig builds the tls.Config for an outbound connection.

InsecureSkipVerify is never set, and there is no option to set it. Pinning is layered on top of chain verification through VerifyPeerCertificate, which Go calls only after the standard checks pass — so a pinned dial is strictly stronger than an unpinned one, never a substitute for it. An operator with a certificate the system store does not trust supplies RootCAFile; that keeps "which authorities do I trust" an explicit, auditable decision instead of "no authorities at all".

func Fingerprint

func Fingerprint(cert *x509.Certificate) string

Fingerprint returns the pin for a certificate: "sha256:" followed by the base64 of SHA-256 over the DER-encoded SubjectPublicKeyInfo.

func FingerprintSPKI

func FingerprintSPKI(spki []byte) string

FingerprintSPKI computes a pin from a raw DER SubjectPublicKeyInfo.

func GenerateSelfSigned

func GenerateSelfSigned(certPath, keyPath string, opts SelfSignedOptions) (pin string, err error)

GenerateSelfSigned writes a fresh P-256 key pair and self-signed certificate to certPath/keyPath and returns the certificate's pin.

This exists for development and for air-gapped labs, not for the public Internet: a self-signed certificate is not trusted by any agent's root store, so the device must be given the certificate explicitly (see the agent's --ca-file). Both files are written through pkg/atomicfile, so a crash mid-write can never leave a certificate that does not match its key — and the key is 0600 from the moment it exists, never briefly world-readable.

func IsLoopbackHost

func IsLoopbackHost(host string) bool

IsLoopbackHost reports whether a hostname refers to this machine.

"localhost" is matched by name because it is not an IP literal, and its resolution to 127.0.0.1/::1 is guaranteed by RFC 6761 for exactly this kind of local-only trust decision.

func LoadServerConfig added in v0.0.5

func LoadServerConfig(certFile, keyFile, minVersion string) (cfg *tls.Config, permWarning string, err error)

LoadServerConfig is ServerConfig without the stderr line: the key-permission problem is returned instead, for a caller that reports it itself. It is the whole of ServerConfig's decision, so a diagnostic that calls it reaches the verdict every listener in the hub reaches — the dashboard's, the git proxy's, the Kubernetes monitor's — rather than a re-derivation of it.

func ParseMinVersion

func ParseMinVersion(s string) (uint16, error)

ParseMinVersion converts a config value into a tls.Version constant.

Accepted: "" (default), "1.2"/"tls1.2"/"tls12", "1.3"/"tls1.3"/"tls13". TLS 1.0 and 1.1 are rejected rather than accepted-with-a-warning: they have no safe configuration left, and an operator who wrote "1.0" into a config file needs to find out at startup, not from a pentest report.

func ParsePin

func ParsePin(pin string) ([]byte, error)

ParsePin decodes a pin string into its raw 32-byte digest.

Both standard and URL-safe base64 are accepted, with or without padding, because pins travel through shells, YAML files and QR codes on their way to an edge device and get mangled in predictable ways. The "sha256:" prefix is optional on input so a bare digest pasted from another tool still works; it is always present on output.

func PinFromCertFile

func PinFromCertFile(path string) (string, error)

PinFromCertFile computes the pin of the leaf certificate in a PEM file.

The leaf is the first CERTIFICATE block, matching how tls.LoadX509KeyPair and every fullchain.pem in the wild are ordered: leaf first, then intermediates. Pinning an intermediate would be a defensible different policy, but silently picking one because it happened to sort first is not.

func ServerConfig

func ServerConfig(certFile, keyFile, minVersion string) (*tls.Config, error)

ServerConfig loads a key pair and returns the tls.Config that `cloop ui` and `cloop serve` serve with.

Both paths are required together: a certificate with no key (or the reverse) is a half-finished configuration, and starting in plaintext because one line of YAML was missing is exactly the silent downgrade this whole task exists to remove.

func ServerConfigFor added in v0.0.5

func ServerConfigFor(getCertificate func(*tls.ClientHelloInfo) (*tls.Certificate, error), minVersion string) (*tls.Config, error)

ServerConfigFor returns the tls.Config ServerConfig would, around a certificate chosen per handshake rather than loaded from files once: one the hub issues itself and replaces while it serves (pkg/hubca, Task 20409). The protocol floor and the cipher suites are the same ones every other listener in the hub serves with, which is the point of building it here.

func VersionName

func VersionName(v uint16) string

VersionName renders a tls.Version constant for logs and error messages.

Types

type ClientOptions

type ClientOptions struct {
	// Pins restricts which server public keys are acceptable. Empty means
	// normal PKI verification only.
	Pins PinSet
	// RootCAFile adds a PEM bundle to the trusted roots, on top of the
	// system store. This is how a development certificate from
	// `cloop hub tls-init` becomes dialable without ever disabling
	// verification.
	RootCAFile string
	// MinVersion overrides the TLS floor. Zero uses TLS 1.2.
	MinVersion uint16
	// ServerName overrides SNI / hostname verification. Empty lets the
	// dialer derive it from the URL, which is what production wants.
	ServerName string
	// Certificates are presented when the server asks for a client
	// certificate — mutual TLS to an audit sink (Task 20426). The private key
	// arrives from the hub's secret store, never from a file the config
	// names.
	Certificates []tls.Certificate
}

ClientOptions parameterises ClientConfig.

type Endpoint

type Endpoint struct {
	// URL is the parsed endpoint.
	URL *url.URL
	// Secure reports whether the scheme provides TLS (wss/https).
	Secure bool
	// Loopback reports whether the host is a loopback address or "localhost".
	Loopback bool
}

Endpoint is the parsed, classified result of checking a dial target.

func CheckEndpoint

func CheckEndpoint(raw string, allowInsecure bool) (ep Endpoint, warning string, err error)

CheckEndpoint applies the plaintext policy to an endpoint.

allowInsecure corresponds to the agent's --insecure-transport flag. When it lets a plaintext non-loopback endpoint through, the returned warning is non-empty and the caller is expected to log it loudly on every connection attempt — a warning printed once at startup scrolls away, and the whole point is that nobody should be able to forget this is on.

func ParseEndpoint

func ParseEndpoint(raw string) (Endpoint, error)

ParseEndpoint validates a WebSocket/HTTP endpoint and classifies it.

It does not decide policy — see CheckEndpoint for that — so callers that only need the classification (the hub's own external URL, for instance) do not have to pretend to be dialling.

func (Endpoint) Host

func (e Endpoint) Host() string

Host returns the hostname without any port.

type PinSet

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

PinSet is a parsed, comparable collection of acceptable server keys.

More than one pin is supported so a certificate rotation onto a *new* key can be staged: publish both pins, roll the server, then retire the old one. Without that, any key rotation is a flag day across the whole fleet.

func ParsePinSet

func ParsePinSet(s string) (PinSet, error)

ParsePinSet parses a comma- or whitespace-separated list of pins. An empty input yields an empty (disabled) set rather than an error, so callers can pass an unset flag straight through.

func (PinSet) Empty

func (p PinSet) Empty() bool

Empty reports whether pinning is disabled for this set.

func (PinSet) Len

func (p PinSet) Len() int

Len returns how many distinct keys this set accepts.

func (PinSet) Matches

func (p PinSet) Matches(cert *x509.Certificate) bool

Matches reports whether cert's public key is pinned by this set.

func (PinSet) String

func (p PinSet) String() string

String renders the set for logs and error messages.

func (PinSet) VerifyPeerCertificate

func (p PinSet) VerifyPeerCertificate() func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error

VerifyPeerCertificate returns a callback for tls.Config of the same name, or nil when the set is empty (pinning disabled — normal PKI verification only).

The callback checks the *leaf*, which is rawCerts[0] by TLS wire order. Go invokes it only after the standard chain and hostname checks have passed, so a match here means "valid certificate AND the expected key", never "invalid certificate we waved through".

type SelfSignedOptions

type SelfSignedOptions struct {
	// Hosts are the DNS names and IP addresses the certificate is valid for.
	// Empty defaults to localhost plus the loopback addresses.
	Hosts []string
	// ValidFor is the certificate lifetime. Zero uses DefaultSelfSignedTTL.
	ValidFor time.Duration
	// Organization appears in the subject. Empty uses "cloop (development)".
	Organization string
	// Now overrides the clock for tests.
	Now func() time.Time
}

SelfSignedOptions parameterises GenerateSelfSigned.

Jump to

Keyboard shortcuts

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