Vier Befunde aus einer externen Durchsicht der 2.4-Integration.
1. Die .htpasswd war eine Klartext-Kundenliste
Das htpasswd-Format hasht nur die Passwortspalte. Benutzername UND Passwort
waren der Lizenzschluessel - der Schluessel stand also im Klartext direkt
neben seinem eigenen bcrypt-Hash, und der Hash war Dekoration. Geschuetzt
hat das Ganze nur die FilesMatch-Regel in derselben Datei.
Der Benutzername wird jetzt abgeleitet: lic_<sha256(schluessel), 16 Hex>.
Die Datei enthaelt damit nur noch eine Einwegableitung und einen Hash ueber
einen hochentropen Schluessel.
Server und SDK muessen dabei zeichengenau uebereinstimmen; ein Test prueft
die C#-Ableitung gegen die PHP-Formel.
2. Ein Formatwechsel blieb unbemerkt liegen
Beim Umbau auf 1. faellt auf: reconcile() sah keinen Anlass zur
Neuerzeugung, die Dateien behielten das alte Format, waehrend die Clients
bereits das neue schickten. Die erzeugten Dateien tragen deshalb jetzt eine
Formatkennung; weicht sie ab, wird neu erzeugt.
3. Doku beschrieb Nginx, der Schutz ist Apache-only
.htaccess wird von Nginx ignoriert - dort waeren die Verzeichnisse offen und
die .htpasswd oeffentlich abrufbar. Die Statusanzeige pruefte nur, ob die
Dateien existieren, und haette in dem Fall "GESCHUETZT" gemeldet.
Neu: ein echter Selbsttest ruft die eigene Paket-Adresse OHNE Zugangsdaten
ab und erwartet 401. Er laeuft beim manuellen Erzeugen und nach jeder
automatischen Neuerzeugung; das Ergebnis steht in der Oberflaeche, ein
Fehlschlag im Log. Er findet nebenbei auch abgeschaltetes AllowOverride und
Tippfehler in der erzeugten Datei. Doku korrigiert, Nginx-Vorlage ergaenzt.
4. Erstinstallation schrieb an einen Ort, an dem Linux-Anwendungen nicht lesen
setup.json-Ziele waren immer installationsrelativ. Eine Anwendung, die sich
unter Linux richtig verhaelt, liest aus $XDG_CONFIG_HOME - /opt/<app> ist
fuer den Dienstbenutzer meist nicht schreibbar. Der Installer legte die
Datei also dorthin, wo nie jemand nachsieht.
Ziele haben jetzt ein "location": install (Vorgabe), config, data, home,
plus ${VAR}- und %VAR%-Ersetzung in "file". Unbekannte Variablen bleiben
stehen statt leer zu werden - ein Platzhalter faellt auf, ein falscher Pfad
nicht. Der Installer gibt den aufgeloesten Pfad aus, weil bei config das
Konto entscheidet, unter dem er laeuft.
Ausserdem
- Doku zeigte "status": "ok" fuer update/delete; Http::ok() erzeugt
"status": "success".
- UPGRADE §16.1 deckte Neuprodukte nicht ab: Fuer ein Produkt ohne Release
existiert /releases/<slug>/ nicht und wird uebersprungen. Das Verzeichnis
entsteht erst mit dem ersten Upload, der naechste Tick schuetzt es. Der erste
ausgelieferte Build muss die Zugangsdaten also schon mitbringen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
121 lines
4.5 KiB
C#
121 lines
4.5 KiB
C#
using System;
|
|
using System.Net.Http;
|
|
using System.Text;
|
|
|
|
namespace Deploymentcenter.Client
|
|
{
|
|
/// <summary>
|
|
/// Zugangsdaten fuer die Release-Ablage.
|
|
///
|
|
/// Die Verzeichnisse unter /releases/ liegen hinter HTTP-Basic-Auth. Wer
|
|
/// herunterladen darf, entscheidet der Lizenzschluessel: Eine Anwendung
|
|
/// mit gueltiger Lizenz versorgt sich damit selbst weiter, ohne dass
|
|
/// zusaetzliche Geheimnisse verteilt werden muessten. Zuvor war die Ablage
|
|
/// offen - und damit auch fuer jeden im Internet herunterladbar.
|
|
///
|
|
/// Bei einer Erstinstallation gibt es noch keinen Schluessel; dort treten
|
|
/// die Zugangsdaten des Installationskontos an seine Stelle.
|
|
/// </summary>
|
|
public sealed class ReleaseCredentials
|
|
{
|
|
private ReleaseCredentials(string user, string password)
|
|
{
|
|
User = user;
|
|
Password = password;
|
|
}
|
|
|
|
public string User { get; }
|
|
|
|
public string Password { get; }
|
|
|
|
/// <summary>
|
|
/// Zugang ueber den Lizenzschluessel.
|
|
///
|
|
/// Der Benutzername wird aus dem Schluessel abgeleitet, das Passwort
|
|
/// ist der Schluessel selbst. Der Grund liegt im htpasswd-Format:
|
|
/// gehasht wird dort nur die Passwortspalte. Stuende der Schluessel
|
|
/// auch als Benutzername in der Datei, waere sie eine vollstaendige
|
|
/// Klartext-Kundenliste und der Hash daneben blosse Dekoration.
|
|
///
|
|
/// MUSS zeichengenau mit ReleaseGuard::licenseUsername() auf dem
|
|
/// Server uebereinstimmen.
|
|
/// </summary>
|
|
public static ReleaseCredentials? FromLicenseKey(string? licenseKey)
|
|
{
|
|
string key = (licenseKey ?? string.Empty).Trim();
|
|
|
|
if (key.Length == 0)
|
|
return null;
|
|
|
|
return new ReleaseCredentials(UsernameForLicenseKey(key), key);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Ableitung des Benutzernamens: "lic_" plus die ersten 16 Hexzeichen
|
|
/// des SHA-256 ueber den Schluessel.
|
|
/// </summary>
|
|
public static string UsernameForLicenseKey(string licenseKey)
|
|
{
|
|
using var sha256 = System.Security.Cryptography.SHA256.Create();
|
|
byte[] hash = sha256.ComputeHash(Encoding.UTF8.GetBytes((licenseKey ?? string.Empty).Trim()));
|
|
|
|
var builder = new StringBuilder("lic_", 20);
|
|
for (int i = 0; i < 8; i++)
|
|
{
|
|
builder.Append(hash[i].ToString("x2"));
|
|
}
|
|
|
|
return builder.ToString();
|
|
}
|
|
|
|
/// <summary>Zugang ueber ein Installationskonto.</summary>
|
|
public static ReleaseCredentials? FromUser(string? user, string? password)
|
|
{
|
|
string u = (user ?? string.Empty).Trim();
|
|
string p = password ?? string.Empty;
|
|
|
|
return u.Length == 0 ? null : new ReleaseCredentials(u, p);
|
|
}
|
|
|
|
/// <summary>Wert fuer den Authorization-Header.</summary>
|
|
public string ToHeaderValue()
|
|
{
|
|
string raw = User + ":" + Password;
|
|
return "Basic " + Convert.ToBase64String(Encoding.UTF8.GetBytes(raw));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Haengt den Header an eine Anfrage. Ohne Zugangsdaten passiert
|
|
/// nichts - eine noch ungeschuetzte Ablage bleibt damit erreichbar.
|
|
/// </summary>
|
|
public static void Apply(HttpRequestMessage request, ReleaseCredentials? credentials)
|
|
{
|
|
if (credentials == null || request == null)
|
|
return;
|
|
|
|
// Nicht doppelt setzen, falls der Aufrufer schon etwas mitgibt.
|
|
if (request.Headers.Contains("Authorization"))
|
|
return;
|
|
|
|
request.Headers.TryAddWithoutValidation("Authorization", credentials.ToHeaderValue());
|
|
}
|
|
|
|
/// <summary>
|
|
/// Erklaerung fuer den Menschen davor, wenn der Server 401 antwortet.
|
|
/// Ohne diesen Hinweis sieht ein abgelaufener Vertrag aus wie ein
|
|
/// Netzwerkfehler, und man sucht an der falschen Stelle.
|
|
/// </summary>
|
|
public static string DescribeUnauthorized(ReleaseCredentials? credentials)
|
|
{
|
|
if (credentials == null)
|
|
{
|
|
return "Die Release-Ablage verlangt Zugangsdaten, es wurden aber keine mitgegeben. "
|
|
+ "Erwartet wird der Lizenzschluessel dieser Installation.";
|
|
}
|
|
|
|
return "Die Release-Ablage hat die Zugangsdaten abgelehnt. Moegliche Gruende: die Lizenz "
|
|
+ "ist abgelaufen, wurde widerrufen oder gehoert zu einem anderen Produkt.";
|
|
}
|
|
}
|
|
}
|