erste module drin.
zwischenstand: Architektur aktualisiert, TOFU implementiert,
This commit is contained in:
1 parent
9d5789c5d7
commit
c8df9d7651
41 files changed
+6455
-36
No files matched your search
@@ -0,0 +1,109 @@
|
||||
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
|
||||
}
|
||||
Reference in new issue
Block a user