110 lines
2.9 KiB
Go
110 lines
2.9 KiB
Go
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
|
|
}
|