Files
br ca6cdcca63 software steht so weit.
jetzt build + docker build und testen
2026-10-02 13:25:34 +02:00

143 lines
4.7 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package crypto
import (
"crypto/rand"
"encoding/base64"
"errors"
"fmt"
"net"
"net/url"
"strconv"
"strings"
)
// TokenPrefix kennzeichnet Enrollment-Tokens.
const TokenPrefix = "sltn_enroll_"
var (
// ErrInvalidToken: Der Token-String hat nicht das Format
// sltn_enroll_<secret>.<fingerprint>[.<server-url>].
ErrInvalidToken = errors.New("crypto: ungültiger Enrollment-Token")
// ErrInvalidServerURL: keine reine https://host[:port]-Adresse.
ErrInvalidServerURL = errors.New("crypto: ungültige Server-URL")
)
const maxServerURLLen = 255
// EnrollmentToken ist die Einladung für einen neuen Agenten.
//
// Secret (inkl. Präfix) ist der geheime Teil: Der Server speichert davon nur
// den Hash (store.AddEnrollmentToken), der Agent schickt es beim Enrollment.
// Fingerprint ist nicht geheim; er sagt dem Agenten, welches
// Server-Zertifikat er erwarten muss. ServerURL (optional, nicht geheim) ist
// die Adresse, unter der der Agent den Server erreicht – eine falsche
// Adresse führt dank Fingerprint-Prüfung höchstens zu einem
// Verbindungsfehler, nie zu einem falschen Server.
type EnrollmentToken struct {
Secret string
Fingerprint Fingerprint
ServerURL string
}
// String liefert die Form, die der Admin an den Agenten weitergibt. Alle
// Teile sind Base64url – der Token ist ohne Quoting shell-sicher.
func (t EnrollmentToken) String() string {
s := t.Secret + "." + string(t.Fingerprint)
if t.ServerURL != "" {
s += "." + base64.RawURLEncoding.EncodeToString([]byte(t.ServerURL))
}
return s
}
// NewEnrollmentToken erzeugt einen Token mit 256 Bit Zufall für den Server
// mit Fingerprint fp; serverURL ist optional ("" = nicht einbetten).
func NewEnrollmentToken(fp Fingerprint, serverURL string) (EnrollmentToken, error) {
if _, err := ParseFingerprint(string(fp)); err != nil {
return EnrollmentToken{}, err
}
if serverURL != "" {
u, err := ValidateServerURL(serverURL)
if err != nil {
return EnrollmentToken{}, err
}
serverURL = u
}
var b [32]byte
if _, err := rand.Read(b[:]); err != nil {
return EnrollmentToken{}, err
}
return EnrollmentToken{
Secret: TokenPrefix + base64.RawURLEncoding.EncodeToString(b[:]),
Fingerprint: fp,
ServerURL: serverURL,
}, nil
}
// ParseEnrollmentToken zerlegt einen Token-String. Leerraum am Rand (beim
// Kopieren) wird ignoriert.
func ParseEnrollmentToken(s string) (EnrollmentToken, error) {
parts := strings.Split(strings.TrimSpace(s), ".")
if len(parts) != 2 && len(parts) != 3 {
return EnrollmentToken{}, fmt.Errorf("%w: erwartet %s<secret>.<fingerprint>[.<server-url>]", ErrInvalidToken, TokenPrefix)
}
secret, rawFP := parts[0], parts[1]
body, ok := strings.CutPrefix(secret, TokenPrefix)
if !ok {
return EnrollmentToken{}, fmt.Errorf("%w: Präfix %s fehlt", ErrInvalidToken, TokenPrefix)
}
if b, err := base64.RawURLEncoding.DecodeString(body); err != nil || len(b) != 32 {
return EnrollmentToken{}, fmt.Errorf("%w: Secret beschädigt", ErrInvalidToken)
}
fp, err := ParseFingerprint(rawFP)
if err != nil {
return EnrollmentToken{}, fmt.Errorf("%w: %w", ErrInvalidToken, err)
}
tok := EnrollmentToken{Secret: secret, Fingerprint: fp}
if len(parts) == 3 {
raw, err := base64.RawURLEncoding.DecodeString(parts[2])
if err != nil || len(raw) == 0 {
return EnrollmentToken{}, fmt.Errorf("%w: Server-URL beschädigt", ErrInvalidToken)
}
if tok.ServerURL, err = ValidateServerURL(string(raw)); err != nil {
return EnrollmentToken{}, fmt.Errorf("%w: %w", ErrInvalidToken, err)
}
}
return tok, nil
}
// ValidateServerURL prüft eine Server-Adresse und normalisiert sie: nur
// https, nur Host und optional Port (kein Benutzer, Pfad, Query, Fragment),
// Host klein, ohne Schrägstrich am Ende.
func ValidateServerURL(raw string) (string, error) {
bad := func(why string) (string, error) {
return "", fmt.Errorf("%w: %q – %s (erwartet https://host[:port])", ErrInvalidServerURL, raw, why)
}
if len(raw) > maxServerURLLen {
return bad("zu lang")
}
u, err := url.Parse(strings.TrimSpace(raw))
switch {
case err != nil:
return bad("nicht lesbar")
case u.Scheme != "https":
return bad("nur https")
case u.Hostname() == "":
return bad("Host fehlt")
case u.User != nil:
return bad("keine Zugangsdaten in der URL")
case u.Opaque != "" || (u.Path != "" && u.Path != "/") || u.RawQuery != "" || u.ForceQuery || u.Fragment != "":
return bad("kein Pfad, keine Query")
}
host := strings.ToLower(u.Hostname())
if port := u.Port(); port != "" {
if n, err := strconv.Atoi(port); err != nil || n < 1 || n > 65535 {
return bad("ungültiger Port")
}
return "https://" + net.JoinHostPort(host, port), nil
}
if strings.Contains(host, ":") {
host = "[" + host + "]"
}
return "https://" + host, nil
}