package auth import ( "bytes" "crypto/ed25519" "crypto/rand" "crypto/sha256" "encoding/base64" "encoding/hex" "errors" "io" "net/http" "strconv" "strings" "time" ) // Header, in denen die Signatur übertragen wird. const ( HeaderKey = "X-Sultan-Key" HeaderTimestamp = "X-Sultan-Timestamp" HeaderNonce = "X-Sultan-Nonce" HeaderSignature = "X-Sultan-Signature" ) // ErrInsecureTransport: Request ohne TLS. Weder wird er signiert noch // akzeptiert. var ErrInsecureTransport = errors.New("auth: unverschlüsselte Verbindung abgelehnt") // signatureVersion trennt die signierten Daten von anderen Ed25519-Nutzungen // desselben Schlüssels und erlaubt spätere Formatänderungen. const signatureVersion = "SULTAN-ED25519-V1" // canonical baut die signierten Daten. Jede Zeile ist eindeutig abgegrenzt; // der Body geht als SHA-256 ein. func canonical(method, requestURI, timestamp, nonce string, body []byte) []byte { sum := sha256.Sum256(body) return []byte(strings.Join([]string{ signatureVersion, method, requestURI, timestamp, nonce, hex.EncodeToString(sum[:]), }, "\n")) } // Signer signiert Requests des Agenten. Sign passt als stream.Client.Prepare. type Signer struct { Key ed25519.PrivateKey now func() time.Time } // Sign signiert r (Methode, Pfad+Query, Zeitstempel, frische Nonce, Body). // Der Body wird dafür gelesen und wiederhergestellt. Nur https-URLs werden // signiert. func (s *Signer) Sign(r *http.Request) error { if len(s.Key) != ed25519.PrivateKeySize { return ErrInvalidKey } if r.URL == nil || r.URL.Scheme != "https" { return ErrInsecureTransport } body, err := readBody(r, -1) if err != nil { return err } if r.Body != nil { r.GetBody = func() (io.ReadCloser, error) { return io.NopCloser(bytes.NewReader(body)), nil } r.ContentLength = int64(len(body)) } now := time.Now if s.now != nil { now = s.now } ts := strconv.FormatInt(now().Unix(), 10) nonce := newNonce() sig := ed25519.Sign(s.Key, canonical(r.Method, r.URL.RequestURI(), ts, nonce, body)) r.Header.Set(HeaderKey, EncodePublicKey(s.Key.Public().(ed25519.PublicKey))) r.Header.Set(HeaderTimestamp, ts) r.Header.Set(HeaderNonce, nonce) r.Header.Set(HeaderSignature, base64.StdEncoding.EncodeToString(sig)) return nil } func newNonce() string { var b [16]byte rand.Read(b[:]) return base64.RawURLEncoding.EncodeToString(b[:]) } // readBody liest den Body vollständig (limit < 0: unbegrenzt) und ersetzt // ihn durch eine Kopie, damit nachfolgende Handler ihn erneut lesen können. func readBody(r *http.Request, limit int64) ([]byte, error) { if r.Body == nil || r.Body == http.NoBody { return nil, nil } rd := io.Reader(r.Body) if limit >= 0 { rd = io.LimitReader(r.Body, limit+1) } body, err := io.ReadAll(rd) r.Body.Close() if err != nil { return nil, err } if limit >= 0 && int64(len(body)) > limit { return nil, ErrBodyTooLarge } r.Body = io.NopCloser(bytes.NewReader(body)) return body, nil }