package moduleregistry import ( "context" "crypto/rand" "crypto/sha256" "crypto/subtle" "encoding/hex" "errors" "fmt" "github.com/jackc/pgx/v5" ) var ( ErrModuleNotRegistered = errors.New("moduleregistry: modul muss vor provisionierung registriert sein") ErrInvalidCredential = errors.New("moduleregistry: ungueltiges oder fehlendes service-credential") ) // Provision stellt ein Service-Credential (Client-ID + Secret) fuer eine // Modul-Instanz aus (Akzeptanzkriterium 4). Das Secret wird NUR beim // Ausstellen im Klartext zurueckgegeben, gespeichert wird ausschliesslich // dessen SHA-256-Hash. func (r *Registry) Provision(ctx context.Context, moduleName string) (clientID, secret string, err error) { if _, err := r.Get(ctx, moduleName); err != nil { if errors.Is(err, ErrModuleNotFound) { return "", "", ErrModuleNotRegistered } return "", "", err } clientID, err = randomToken(16) if err != nil { return "", "", fmt.Errorf("client-id erzeugen: %w", err) } secret, err = randomToken(32) if err != nil { return "", "", fmt.Errorf("secret erzeugen: %w", err) } hash := hashSecret(secret) _, err = r.pool.Exec(ctx, ` INSERT INTO module_credentials (module_name, client_id, secret_hash, issued_at) VALUES ($1, $2, $3, now()) ON CONFLICT (module_name) DO UPDATE SET client_id = $2, secret_hash = $3, issued_at = now() `, moduleName, clientID, hash) if err != nil { return "", "", fmt.Errorf("credential speichern: %w", err) } return clientID, secret, nil } // Authenticate prueft ein Service-Credential timing-safe (Referenzmuster // siehe AUD-02) — Aufrufe ohne gueltiges Credential werden abgelehnt // (Akzeptanzkriterium 4 / Pruefung 4). func (r *Registry) Authenticate(ctx context.Context, clientID, secret string) (moduleName string, ok bool, err error) { if clientID == "" || secret == "" { return "", false, nil } var storedHash []byte err = r.pool.QueryRow(ctx, ` SELECT module_name, secret_hash FROM module_credentials WHERE client_id = $1 `, clientID).Scan(&moduleName, &storedHash) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return "", false, nil } return "", false, fmt.Errorf("credential lesen: %w", err) } if !timingSafeEqual(hashSecret(secret), storedHash) { return "", false, nil } return moduleName, true, nil } func randomToken(n int) (string, error) { buf := make([]byte, n) if _, err := rand.Read(buf); err != nil { return "", err } return hex.EncodeToString(buf), nil } func hashSecret(secret string) []byte { sum := sha256.Sum256([]byte(secret)) return sum[:] } // timingSafeEqual folgt derselben Referenzimplementierung wie AUD-02 // (subtle.ConstantTimeCompare) — projektweite Konvention fuer jeden // sicherheitsrelevanten Vergleich. func timingSafeEqual(a, b []byte) bool { if len(a) != len(b) { return false } return subtle.ConstantTimeCompare(a, b) == 1 }