2FA TOTP auf legacy tcp services ohne zentralen SPoF

reinhard@finalmedia.de Tue Mar 15 20:43:27 PM CET 2026

KISS. Ohne LDAP, Radius, TwinGate, Duo, Keycloak, Authelia, SAML oder OIDC

Dir gefällt die Lösung und du willst Danke sagen? Spendier mir nen Kaffee.

Achtung: Dieses Tutorial setzt das Tutorial 679c3684bdec voraus, in der ich eine minimalistische verzeichnisdienst serverinfrastruktur mit /etc/account Struktur konzipiert und vorgestellt habe, hier genannt "cmd server". Der cmd übernimmt hier auch die Funktion des Signatur-Servers. Mit hinreichendem Verständnis der Gesamtkonzepte, kannst du es aber auch ohne diese Verzeichnisstruktur auf einem dedizierten Signaturserver realisieren. Das Konzept ist flexibel genug, um dies zu ermöglichen.

Beachte zudem: Es ist alternativ zu dem im separaten Tutorial 32f41290295d beschriebenen, reinen webbasierten httpauth legacy 2FA Konzept zu verstehen und daher auch auf Services anwendbar, die keinen zentralen Auth oder Webdienstbindung bereitstellen. Also: Das aktuelle Tutorial 95ab08946d60 ist primär für Really-Really-Legacy Based Stuff, der direkt mit TCP basierten Services arbeitet und am einfachsten wie nachfolgend beschrieben werden kann:

Challenge und Szenario: Eine unveränderbare alternativlose closed source legacy Applikation unter Windows soll nachträglich 2FA Fähigkeiten erhalten. Die Legacy Applikation hat dabei direkten Datenbankzugriff auf eine legacy mysql Datenbank und ist ohne diese nicht lauffähig. Die Verbindung zwischen Applikation und Datenbank soll nun um eine 2FA ergänzt werden, ohne in die Legacy Applikation eingreifen zu müssen und ohne das bestehende Schema der Datenbank zu verändern, zu erweitern oder am eigentlichen Datenbank-Login Änderungen vorzunehmen. Das Ganze muss möglich kostengünstig sein und es sollen auch keine Schulungen zu neuer Software oder Infrastruktur nötig werden.

Lösung: 2FA Konzept für Legacy Dienste, die nicht als Webdienst realisiert werden können

Realisierung als: Universal 2FA TOTP mit ssh enterprise user cert infrastruktur ohne X509

Bezweckt: duale/separierte Laufzeit - Einerseits für Short Session Signatur-Legitimitation und andererseits für Long Session auf Basis der Signatur in Kombination.

Verwendungszweck: Nutzbar für Legacy Dienste, die allgemein nicht an LDAP, Radius, Microsoft NPS, TwinGate, Duo, Keycloak, Authelia oder simple WebProxy gebunden werden können, sondern eine interne eigene Benutzeraccounts verwenden, die keine 2FA Unterstützung mit sich bringt und auch nicht nachgerüstet werden kann.

Prämisse: Wirtschaftlichkeit, Zweckmäßigkeit, Resilienz und Einfachheit sollen im Fokus stehen.

Implementierungsansatz: Alles mit Bordmitteln nach KISS zu realisieren. Unter Windows/Linux/Mac nutzbar. Verwendbar für beliebige Legacy Services, die z.B. eine Datenbank-Anbindung benötigen, um zu funktionieren. Aber auch andere TCP Kapselungen sollen möglich sein. Verzicht auf zentrale Dienste wie Radius oder VPN, da diese sonst SPoF sind und dort bei Ausfall, Update oder Wartung generell *alle* Dienste nicht mehr nutzbar wären. Das gilt es im Rahmen des Resilienzforderung zu vermeiden.

Detaillierte Konzeptbeschreibung am Beispiel und Kontext für legacyapp die direkten mysql Datenbank Zugriff benötigt

Optimierte Wirtschaftlichkeit, Resilienz, Compliance, Flexibilität und Wartbarkeit

Der Workflow

Compliance Sicherstellung Gültigkeitslimit

Einschränkung:

Mit dem 2FA wird in obigem Beispiel für den Endnutzer allgemein Zugriff auf das SQL Interface des mysql Servers gestattet. Das Konzept funktioniert also nur, wenn dort nicht andere zu isolierende Datenbanken genutzt werden. Der separate zusätzliche Datenbank Auth bleibt natürlich bestehen, als zusätzliche Schicht. Um auch diese Einschränkung zu lösen, besteht eine einfache Möglichkeit: Man muss eine andere Loopback Adresse pro Datenbank verwenden und den mysql server darauf konfigurieren, dass die Datenbank nur über diese Adresse erreichbar ist. Damit kann man also statt 127.0.0.1 z.B. 127.0.0.2 etc. nutzen. In unserem Fall aber nicht relevant. Alternativ kann man auch einen anderen Datenbank Port binden und den dann im Forwarding explizit für den namespace freigeben. Also ein sehr flexibles Konzept.

Vorteile

Durch die ssh Tunnelanbindung kann theoretisch auch ein TLS Zugriff entfallen (wenn überhaupt in der legacy Applikation vorhanden) und man kann den SQL Server notfalls auch ohne X509 Certs betreiben, da der Zugriff nur über Loopback und den separaten SSH Tunnel Mantel erfolgt. Das konzept ist damit auch für Legacy Systeme nutzbar, die keine TLS verschlüsselten Datenbankanbindungen beherrschen, oder wenn auch beliebigen Gründen ein X509 nicht möglich ist, oder X509 Zertifikate nicht erneuert werden könnten. Dies bringt damit auch Resilienz gegenüber WebProxy Verfahren, die zwingend an eine X509 Zertifikatinfrastruktur gebunden sind.

Klartext zum Klartext

Das 2FA shared secret muss dem cmd Server bekannt sein, sonst kann er nicht gegenprüfen. Daher ist es dort im Klartext vorhanden.

Einzige Möglichkeit wäre noch, dass es zusätzlich gpg verschlüsselt wäre. Dann müsste es der Nutzer beim Signieren zur Laufzeit entschlüsseln und dem Server dann wiederum zur Prüfung innerhalb einer Session als Klartext vorlegen. Das wäre jedoch fatal, denn es ermöglicht eine Injection beliebiger shared secrets von Nutzerseite, daher kann dieses Verfahren nicht verwendet werden. Man müsste es bei einer Verschlüsselung also durch einen zusätzlichen separaten PIN/Key symmetrisch verschlüsseln und hashen, was alles weiter verkompliziert und für den Administrator und Nutzer schwerer nachvollziehbar und somit unverständlich macht.

Auch die direkt Möglichkeit das shared Secret auf dem Server nur als hash gespeichert wird scheidet aus, da sonst oathtool keine Vergleichsmöglichkeit hat. Der Vergleich für oathtool würde dann nur für ein vom nutzer wiederum selbst bei der signatur mitgeteilten shared secret basieren und der nutzer müsste das shared secret bei jedem Erneuern auch noch mit übertragen. Der Server hätte dies nur initial als hash gespeichert, würde dann das übermittelte shared secret wiederum hashen, den hash vergleichen und nur wenn das korrekt ist, dann auch direkt durchwinken. Allerdings hebelt dies ja das totp verfahren aus, da das shared secret auf der Nutzerseite außerhalb des Authenticators sicher verwahrt werden müsste. Was ja schwieriger ist und überhaupt der Grund für das 2FA ist.

Der Server muss daher zwingend Hohheit über das shared Secret besitzen und es daher auch im Klartext kennen. Wir stellen auch die Sicherheit des shared secrets sicher, indem wir ein langes Zufallspasswort dafür wählen und es nicht den Nutzer wählen lassen. Es bestünde auf Serverseite als Schutzmaßnahme des shared secrets daher lediglich die Möglichkeit, die datei in einen separaten Trust-Store auszulagern (HSM), was aber hohe Komplexität erzeugt und das Problem nur verlagert, da es dem Server ja dennoch im Klartext bekannt sein muss.

Fazit und sicherheitstechnische Bewertung

Ein vollständiger Zero-Knowledge-Ansatz, also Pake-Protokolle, sind generell mit TOTP nicht machbar, aber auch nicht Ziel des Konzepts. TOTP basiert zwingend auf einem geteilten Geheimnis als symmetrischen Schlüssel. Das ist aber auch in Ordnung. Da es sich allerdings im gesamten Verfahren ja nur um einen Faktor, der gesamten Authorisierung handelt, ist das Risiko als vertretbar einzustufen. Das bedeutet als Gesamtbild konkret:

Für den Faktor 1 (TOTP) ist es also so, dass der Server Klartext hat, der Nutzer den Klartext ebenso kennt, diesen aber in seinem Authenticator sicher verwahrt, weil es dort nicht auslesbar ist, sondern nutzerseitig im Alltag ausschließlich als abgeleiteter TOTP PIN verwendet wird. Und für den Faktor 2 (reguläres Passwort danach) ist es so, dass der Server das Passwort ohnehin nur als Hash vorhält und der Nutzer den Klartext bereitstellt (als Passwort). Durch die Kombination beider Verfahren über Kreuz ist auch das Risiko verteilt. Da auch die Kapselung noch in SSH Tunnel mit asymmetrischen Keys und generell passwortlos und keybasiert stattfindet, stellt das für die Verbindung einen weiteren Schutz dar. Auch wird der ssh privatekey des Endnutzers ohnehin noch zusätzlich durch eine symmetrische Passphrase auf der clientseite geschützt, was als Faktor 3 zu sehen, den aber der User selbst garantieren muss, indem er keine unsichere oder leere Passphrase setzt.

IMPLEMENTIERUNG - Konkrete Realisierung in fünf Teilen A-E

[A] mysql server dortige /etc/sshd_config ergänzen

Hinweis: der spezielle hier in der Direktive "TrustedUserCAKeys" genannte publickey in der file /etc/ssh/ca.legacyapp.key.pub wird erst in Schritt D hierher einmalig übertragen. Die Konfiguration der /etc/sshd_config kann jedoch schon so ergänzt werden, ist aber erst später aktivierbar, wenn auch die keyfile dort liegt. Auf Seite des mysql Servers ist dann keine weitere Anpassung nötig und es müssen dort auch keinerlei user Keys gepflegt werden!


	Match User legacyapp
	    AuthenticationMethods publickey
	    TrustedUserCAKeys /etc/ssh/ca.legacyapp.key.pub
	    ForceCommand "/usr/bin/timeout -k 2 28800 /bin/sleep 28800"
	    ChannelTimeout direct-tcpip=28800
	    ClientAliveInterval 10
	    AllowTcpForwarding yes
	    PermitTTY no
	    X11Forwarding no
	    AllowAgentForwarding no
	    PermitOpen 127.0.0.1:3306

[B] legacyapp user windows seite in powershell script "legacyappsign.ps" zum TOTP Management

### 1. Dem Nutzer einen grafischen Prompt anzeigt. Alles andere ist im Script
### vorausgefüllt mit seinen nutzerspezifischen Werten,
### sodass er nur noch seine aktuelle TOTP PIN eingeben muss
### das u999999 ist durch die KORREKTE masterid des Nutzers im Script in der Variable
### zu Beginn hier im Script zu ersetzen! Sonst bleibt alles für alle user gleich.

	$prefix="u999999:legacyapp:"
	certPath = "$HOME\.ssh\legacyapp-user-cert.pub"

	Add-Type -AssemblyName Microsoft.VisualBasic
	$pin = [Microsoft.VisualBasic.Interaction]::InputBox("Bitte aktuelle PIN eingeben", $prefix, "")
	if ([string]::IsNullOrWhiteSpace($pin)) { exit }
	$payload = ("$prefix" + "$pin").Trim()
	if ($payload.Trim() -notmatch "^[0-9a-z]{1,32}:[0-9a-z]{1,32}:[0-9]{1,6}$") { exit }


### 2. Eigenen ssh Key  signieren lassen (stdin/stdout), dazu beim login an signierserver
### aktuellen totp pin auf stdin einreichen und 8 stunden gültigen cert pubkey auf stdout zurückerhalten, 
### sofern der totp pin korrekt. die datei muss auf -cert.pub enden damit sie vom
### ssh client automatisch gefunden wird. oder man nutze die Option CertificateFile=
### wie wir das hier in den tunnelargs tun.

	$cert = $payload.Trim() | ssh sign@cmd 
	if ($cert) { $cert | Out-File $certPath -Encoding ascii } else { exit }

### 3. Den mysql-Tunnel für legacyapp mit certpubkey im Hintergrund starten. 
### dabei wird ssh automatisch die cert file nutzen. ssh terminal wird verkleinern 
### aber im hintergrund aktiv halten.

	Start-Process legacyapptunnel.bat -WindowStyle Minimized

### 4. legacyapp starten in Vordergrund (Beispielhaft)

	Start-Process "C:\legacyapp\legacyapp.exe"

[C] legacyapp user windows seite in Batch script "legacyapptunnel.bat" zum Tunnelaufbau

Das Batchscript legacyapptunnel.bat dient dazu, den eigentlichen SSH Tunnel direkt zum mysql server zu starten. Es sieht als Einzeiler wie nachfolgend aus und kann (falls gültiges und nicht abgelaufenes signiertes cert vorhanden) jederzeit separt manuell aufgebaut werden. Daher ist er als separate Batch file ausgelagert, sodass man ihn 8h lang immer unabhängig vom Signaturprozess erneut starten kann, wenn z.B. die Netzwerkverbindung unterbrochen wurde:


	@echo off
	ssh -vv -o ServerAliveInterval=10 -o CertificateFile="%userprofile%\.ssh\legacyapp-user-cert.pub" -L 3306:127.0.0.1:3306 -N legacyapp@mysql-server

[D] signaturprozesse auf cmd vorbereiten

Einmalig auf cmd server vorbereiten

	apt-get install oathtool qrencode daemontools

	useradd --system -d /home/sign -s /bin/sh sign
	openssl rand -hex 64 | passwd --stdin sign
	mkdir /home/sign
	chown sign:sign /home/sign

	find /etc/accounts/1/ -mindepth 1 -maxdepth 1 -type d -exec mkdir -p "{}/sign" \;
	find /etc/accounts/1/ -mindepth 2 -maxdepth 2 -type d -name "sign" -exec chown sign:root -R "{}" \;

Einmalig global für jeden namespace eine ca vorbereiten. hier für den namespace "legacyapp", den dann jeder account nutzer verwenden kann. Das muss auf cmd durch den systemuser sign erfolgen


	su sign
	echo | ssh-keygen -t ed25519 -f /home/sign/ca.legacyapp.key -C "ca.legacyapp"
	exit	

Den ca publickey(!) muss der IT-Admin nun einmalig zum mysql Server übertragen, damit er dort nutzbar ist.

	scp cmd:/home/sign/ca.legacyapp.key.pub mysqlserver:/etc/ssh/ca.legacyapp.key.pub

Nun auf dem cmd server für jeden Endnutzer, der später den legacyapp namespace zur signatur nutzen darf, eine Expiry setzen. Erst damit wird die Nutzung für den User gestattet. Hier ab Beispiel für den fiktiven u999999, die ihm das Signieren von 8 Stunden gültigen ssh Zertfikaten für den namespace legacyapp erlaubt.

	echo +8h > /etc/accounts/1/u999999/sign/legacyapp.expiry

Beachte: Das + muss angegeben werden. Zum Verständnis: Der Inhalt der Datei ist später der Übergabewert an das -V Flag von ssh-keygen. Der Parameter ist in der ssh-keygen Doku so definiert:

"Ein Gültigkeitsintervall kann aus einer einzelnen Zeit bestehen, die angibt, dass das Zertifikat ab jetzt gültig ist und zu dieser Zeit abläuft. Es kann auch aus zwei durch Doppelpunkte getrennten Zeiten bestehen, die ein explizites Zeitintervall anzeigen."

Beispiele laut Manual für etwaige Expiry Werte sind also


	+52w1d
	Gültig von jetzt bis 52 Wochen und ein Tag von jetzt.

	-4w:+4w
	Gültig von vor vier Wochen bis zu vier Wochen von jetzt.

	20100101123000:20110101123000
	Gültig vom 1. Januar 2010, 12:30 Uhr bis 1. Januar 2011, 12:30 Uhr.

	20100101123000Z:20110101123000Z
	Ähnlich, aber in der UTC-Zeitzone statt der Systemzeitzone interpretiert.

	-1d:20110101
	Gültig von gestern Mitternacht, 1. Januar 2011.

	0x1:0x2000000000
	Gültig von grob Anfang 1970 bis Mai 2033.

	-1m:forever
	Gültig von vor einer Minute und niemals ablaufend.

Mein Konzept sieht 8h vor, denn Rahmen der 2FA Nutzung ist eine Sessiontime von 8h für Auditoren oft das vertrebare Maximium, da es einem Arbeitstag entspricht. Kommt also bitte nicht in die Versuchung, hier forever zu setzen, denn dann müssten man im Falle einer Kompromittierung auch das ca cert revoken und austauschen! Das Einzige, was noch denkbar wäre in diesem Rahmen ist daher "-10m:+8h", für den Fall, dass ein Server des namespaces eine derart hohe Zeitdrift hätte. Kürzere Zeiträume sind natürlich jederzeit möglich und auch kein Problem. Dann muss man aber auch hier für ein forciertes Trennen etwaiger bestehender Verbindungen sorgen.

Wie gehts es weiter mit der Realisierung auf dem cmd server?

Damit das nun alles funktioniert, ist auf dem cmd server ist in der /etc/ssh/sshd_config dann einmalig die nachfolgende Config zu ergänzen und mit dem Befehl service ssh reload zu aktivieren.

Match User sign
        AuthorizedKeysCommand /etc/accounts/.show.ssh.active.authorizedkeys /etc/accounts/.ssh.sign.totp %t %k %f
        AuthorizedKeysFile none
        AuthorizedKeysCommandUser root
        AllowTcpForwarding no
        X11Forwarding no
        AllowAgentForwarding no

Es muss auch daran gedacht werden, den neuen system user "sign" in die Liste der AllowUsers aufzunehmen, die ja in der /etc/ssh/sshd_config separat definiert ist. Zudem ist zu kontrolieren, das auch die Direktive FingerprintHash md5 in der globalen config korrekt gesetzt ist. Sollte der Fall sein.

Das in der obigen config referenzierte Script /etc/accounts/.ssh.sign.totp führt dann on demand den eigentliche Signatur- oder Registrationsprozess durch, wenn man sich als user "sign" auf cmd via ssh einloggt will.

Dazu nutzt es dann totp mit oathtool zum jeweiligen shared secret, das einen 30s gültiges Token zum Signieren erfragt und validiert.

Eine Signatur ist nur möglich, wenn der via ssh anfragende key zum fingerprint der für die masterid definierten publickey passt, also die Signaturmöglichkeit administrativ freigegeben wurde - und passend für den definierten masterid user ein shared secret gibt und vom nutzer das für diesen moment passende und korrekte totp pin mit übermittelt wurde. Nur dann erzeugt der signaturserver ein openssh cert pubkey auf stdout.

Gibt es noch kein shared secret für den vom nutzer gewählten namespace in seinem masterid unterordner "sign", so erzeugt das script eines für den namespace und gibt es als otp qrcode einmalig zurück. dabei ist als registrations totp pin dann einmalig die 000000 zu verwenden, die ansonsten aber nirgends einfließt und nur als padding dient. Nur durch administrativen Eingriff (entfernen der datei im sign folder des users) kann der Nutzer erneut ein anderes shared secret setzen. Konkret:

Zur einmaligen Erzeugung würde der Windows- oder Linuxnutzer einfach einmalig diesen Einzeiler nutzen, falls er die fiktive masterid u999999 hätte:


	echo u999999:legacyapp:000000 | ssh -v sign@cmd

Und das funktioniert nur, wenn für den user und seine masterid auch administrativ zuvor ein expiry file für den namespace angegeben wurde und zudem nur, wenn die kombination aus masterid und verwendeten ssh publickey stimmt. der publickey muss also in /etc/accounts/1/u999999/ssh.pubkeys/md5.xxxxxx existieren, wobei xxxxxx der fingerprint und es muss /etc/accounts/1/u999999/sign/legacyapp.expiry existieren.

Womit der Nutzer also, wenn er mit seinem bestehenden ssh pubkey authorisiert ist sich einloggen darf. Falls in seinem account verzeichnis kein shared secret in der Datei /etc/accounts/1/u999999/sign/legacyapp besteht, also die datei noch nicht existiert dann ein neues secret in diese Datei generiert wird und ihm der qrcode einmalig zum Scannen in seinem Authenticator angezeigt wird.

Das Registrationsverfahren kann er nicht mehr nutzen, sobald die Datei existiert. Auch kann die Datei administrativ geschützt werden mit chattr +i etc. Tipp: Das -v ist sinnvoll, um im Fehlerfall auch den detaillierten Exitcode des serverseitigen Scripts zu sehen! Damit sieht man genau, aufgrund welcher Umstände abgebrochen wurde. Aber es ist optional.

Zur Signatur nutzt der Endnutzer dann nochmal den gleichen Befehl, übergibt dann aber die aktuell gültige TOTP PIN und erhält auf stdout ein signiertes Zertifikat zurück. Beispiel


	echo u999999:legacyapp:123456 | ssh -v sign@cmd > legacyapp-user-cert.pub

Tipps, Tricks und Verständnishilfen

Ein signiertes ssh-ed25519-cert-v01@openssh.com user certificate kann ein Endnutzer später jederzeit selbst prüfen mittels


	cat legacyapp-user-cert.pub | ssh-keygen -L -f-

Er bekommt damit auch die Gültigkeitsspanne, Ablaufzeitpunkt, Principals (Namespace) und die in der info codierte masterid und fingerprint des passenden publickeys ausführlich angezeigt.

Und ein IT Administrator kann sich auch die aktuell gültige und alle 30s wechselende TOTP Pins des Endnutzers berechnen lassen, indem er nachfolgenden Befehl nutzt und aus dem shared Secret auf der Serverseite ableitet.


	watch "oathtool -b --totp @/etc/accounts/1/u999999/sign/legacyapp"

auch kann ein IT-Admin jederzeit wieder einen QR generieren, falls der Endnutzer diesen verloren hat:


	url="otpauth://totp/u999999:legacyapp?issuer=legacyapp\&secret="
	cat /etc/accounts/1/u999999/sign/legacyapp | tr -dc "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567" | \
	sed "s|^|${url}|g" | qrencode -t utf8

Beachte: Das tr cleaning ist dabei wichtig, da keine "=" PADDING Trails enthalten sein dürfen.

und kann dem Nutzer diesen QR-Code auf sichere Weise zukommen lassen. Dabei können auch png grafiken erstellt werden, oder einfach nur die URL. Auch kann ein IT-Admin einfach das shared secret auf dem cmd server löschen:


	chattr -i /etc/accounts/1/u999999/sign/legacyapp
	rm /etc/accounts/1/u999999/sign/legacyapp

worauf sich der Endnutzer selbst ein neues Erstellen kann, wenn er die 000000 pin Variante wie oben bereits beschrieben erneut nutzt.

[E] auf signierserver (cmd) das eigentliche Script /etc/accounts/.ssh.sign.totp

Das ist das wichtige Script auf dem cmd Server, mit der eigentlichen magic, damit all das oben beschriebene funktioniert.

#!/bin/sh
# reinhard@finalmedia.de 20260217

# /etc/accounts/.ssh.sign.totp %t %k %f

# parameter in sshd_config für keytype, pubkey, fingerprint
# liest zudem zwingend ZUSÄTZLICH bei ssh connect von stdin eine zeile in der form "masterid:namespace:pin"
# die wiederum vom nutzer parametrisiert auf stdin als Zeile übergeben wird.

#  DAMIT DAS SCRIPT FUNKTIONIERT, müssen folgende Voraussetzungen erfüllt sein:
#
#  apt-get install oathtool qrencode daemontools
#  useradd --system -d "/home/sign" -s /bin/sh "sign"
#  openssl rand -hex 64 | passwd --stdin "sign"
#  mkdir /home/sign
#  chown sign:sign /home/sign
#
# A) Zudem MUSS in jedem jeweiligen Endnutzer Account Verzeichnis /etc/accounts/1/u999999/
# jeweils ein Unterordner "sign" existieren, der dem systemuser "sign" gehört!
# find /etc/accounts/1/ -mindepth 1 -maxdepth 1 -type d -exec mkdir -p "{}/sign" \;
# find /etc/accounts/1/ -mindepth 2 -maxdepth 2 -type d -name "sign" -exec chown "sign:root" -R "{}" \;
# dort werden pro user die totp shared secrets abgelegt, um die sign funktion nutzen zu können
# Integriere das auch in eine skeleton file, damit das künftig immer mit erstellt wird bei neuen Endnutzern!

# B) Damit ein Endnutzer überhaupt in einem namespace signieren darf, MUSS in SEINEM "sign" verzeichnis auch
# zwingend auch eine datei liegen, die den expiry für den namespace definiert und das Suffix ".expiry"
# trägt. Daher dürfen namespaces auch keine punkte enthalten. Das gestattet es dem Administrator auch,
# für Endnutzer bei Bedarf unterschiedliche expiry zu definieren und regelt in einem Rutsch generell
# die Berechtigung des Endnutzers zur Durchführung eigener Signaturen für den Namespace.
# Beispiel für Namespace legacyapp:  echo "+8h" > /etc/account/1/u999999/sign/legacyapp.expiry
# Die expiry Datei sollte nicht von sign schreibbar, aber lesbar sein. idealerweise also von root
# angelegt als global lesbar, also chmod 644 und mit chattr +i versehen.
# Beachte: das suffix heißt .expiry nicht .expire !

# C) Desweiteren muss "/home/sign/" existieren und dem systemuser "sign" gehören.
# Dort werden zentral jeweils pro namespace ein ca privatekey zum signieren abgelegt.
# Die Dateien sind benannt nach dem jeweiligen namespace. Schema ist "ca.namespace.key", also ca vorangestellt
# und .key als suffix. Die dortigen Dateien müssen dem User "sign" gehören, bzw. er muss sie lesen können.
# Da die Datei sensibel ist, sollte NUR der user sign sie lesen dürfen. also chmod 600 und chown sign:root
# und auch ein chattr +i darauf um sie vor schreibzugriffen zu schützen. In einem Audit-log sollten diese
# keys zudem überwacht werden und alle Zugriffe darauf protokolliert werden!
# Beispiel Erstellung für Namespace "legacyapp":  echo | ssh-keygen -t ed25519 -C "ca.legacyapp" -f /home/sign/ca.legacyapp.key

# Tipp: Ein signiertes ssh-ed25519-cert-v01@openssh.com user certificate kann ein Endnutzer
# später jederzeit selbst prüfen mittels  cat datei-cert.pub | ssh-keygen -L -f-

# +++ Beginn +++
# Abbruch, wenn das Script unparametrisiert aufgerufen wurde
test $# -ge 3 || exit 64

# info für user auf stderr ausgeben
echo "please enter masterid:namespace:totp" >&2
echo "hint: (once) register new secret in namespace by choosing 000000 as totp pin" >&2

# via stdin vom user übergebene werte auslesen und hart das charset limitieren. 
# keine slashes und keine Punkte erlauben! Nur 0-9a-z für die masterid und namespace
# und nur 0-9 für die pin. Diese Prüfungen sind SEHR WICHTIG.
export line="$(timeout 30 head -n1 | tr -dc "0-9a-z:")"
export masterid="$(echo "$line" | cut -d: -f1 | tr -dc "0-9a-z" | head -c 32)"
export namespace="$(echo "$line" | cut -d: -f2 | tr -dc "0-9a-z"  | head -c 32)"
export pin="$(echo "$line" | cut -d: -f3 | tr -dc "0-9"  | head -c 8)"

# serverseitig übergebene werte auslesen
export fingerprint="$(echo "$3" | cut -d: -f2- | tr -dc "0-9a-f"  | head -c 128)"
export pubkey="$(echo "$1 $2" | tr -dc "0-9a-zA-Z /+=-")"


# Abbruch wenn keine gültigen Werte
test -n "${masterid}" || exit 65
test -n "${namespace}" || exit 66
test -n "${pin}" || exit 67
test -n "${fingerprint}" || exit 68


# +++ Legitimationsprüfung +++  (SEHR WICHTIG!!)
# Sicherstellen, dass der zum login verwendete ssh publickey auch
# wirklich zum angegebenen masterid user gehört und bereits registriert ist.
# Ansonsten direkt abbruch
test -f "/etc/accounts/1/${masterid}/ssh.pubkeys/md5.${fingerprint}" || exit 70
grep -q "${pubkey}" "/etc/accounts/1/${masterid}/ssh.pubkeys/md5.${fingerprint}" || exit 71


# Herausfinden, ob und welche Ablauffrist für den user und den namespace definiert wurde
# Sofortiger Abbruch, wenn kein expiry definiert, weil dann der user auch den namespace
# nicht nutzen darf und dafür keine certs signieren dürfte.
export expiryfile="/etc/accounts/1/${masterid}/sign/${namespace}.expiry"
test -f "${expiryfile}" || exit 72
export expire="$(head -n1 "${expiryfile}" | tr -dc "0-9a-zA-Z:+-")"
test -n "${expire}" || exit 73


# Nun alle für die Signatur wichtigen Variablen definieren
export cakeyfile="/home/sign/ca.${namespace}.key"
export secretfile="/etc/accounts/1/${masterid}/sign/${namespace}"
export url="otpauth://totp/${masterid}:${namespace}?issuer=${namespace}\&secret="


# +++ Register MODE +++
# Falls secretfile noch NICHT existiert, und pin 000000 übergeben wurde,
# dann neues shared secret erstellen aus Zufallspasswort. Dieses Passwort base32 encodiert
# in secretfile datei und explizit für den user speichern.
# dann totp auth url ausgeben (WICHTIG ohne padding, also ohne = Zeichen in base32)
# nochmal als QR Code ausgeben (dabei ebenso ohne padding, also ohne = Zeichen in base32)
# dann beenden! Mit statuscode 0.
umask u=rw,g=,o=
test "${pin}" = "000000" && test ! -f "${secretfile}" && openssl rand 32 | base32 > "${secretfile}" && echo && \
tr -dc "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567" < "${secretfile}" | sed "s|^|${url}|g" && echo && echo && \
tr -dc "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567" < "${secretfile}" | sed "s|^|${url}|g" | \
qrencode -t utf8 && echo && echo && exit 0


# +++ Validate MODE +++
# Dieser Modus wird nur erreicht, wenn auch bereits alle Prüfungen erfolgt sind.
# Abbruch, wenn wir kein shared secret des users für den namespace vorliegen haben
test -f "${secretfile}" || exit 80

# Abbruch, wenn wir für den gewünschten namespace keinen zum Signieren nutzbaren ca key vorliegen haben
test -f "${cakeyfile}" || exit 81

# übergebene totp pin gegen secretfile prüfen. Abbruch im Fehlerfall.
# Das ist die eigentliche PIN Prüfung!
oathtool -w 2 -b -s 30s -d 6 --totp "@${secretfile}" "${pin}" >&2 || exit 82


# +++ Sign MODE +++
# Wenn soweit alles ok, dann aus den übergebenen werten und fingerprint,
# sowie den verwendeten ssh publickeys zwei hashes erzeugen,
# um einen sicheren, temporär eindeutigen aber im kontext gleicher authentifizierung notfalls
# replizierbaren Namen für die pubfile zu definieren. Ablage des Publickeys in der ramdisk.
# da ssh-keygen zum signieren nur auf files und nicht auf Standardströmen arbeiten kann
# und das cert file immer im gleichen ordner unter dem festen suffix -cert.pub ablegt.
# geht nur so, weil dies in ssh-keygen so realisiert ist. wir löschen die datei im anschluss.

export hashusr="$(echo "${masterid}${namespace}${pin}" | sha256sum | tr -dc "0-9a-f")"
export hashsrv="$(echo "$1$2$3" | sha256sum | tr -dc "0-9a-f")"
export pubfile="/dev/shm/sign.${hashusr}.${hashsrv}.$(date +'%s%N')"
export usrcrtfile="${pubfile}-cert.pub"

# den in der aktuellen session benutzten ssh publickey in temporäre pubfile sichern
echo "${pubkey}" | grep "^ssh-" | cut -d " " -f 1,2 > "${pubfile}" || exit 83

# Sicherstellen, dass pubfile geschrieben wurde. Abbruch im Fehlerfall.
test -s "${pubfile}" || exit 84

# ssh zertifikat für den publickey mit begrenzter laufzeit signieren und
# das user certificate auf stdout ausgeben. Abbruch im Fehlerfall.
ssh-keygen -s "${cakeyfile}" -I "${masterid}:${fingerprint}" -n "${namespace}" -V "${expire}" "${pubfile}" >/dev/null 2>/dev/null && \
test -f "${usrcrtfile}" && cut -d " " -f1,2 "${usrcrtfile}" && rm "${pubfile}" "${usrcrtfile}" || exit 85

# Sauber beenden und neben Zertifikat auch einen menschen lesbaren Comment auf stderr ausgeben
date +"%Y%m%d%H%M%S signed cert masterid:${masterid} namespace:${namespace} expire:${expire} usable for ${fingerprint}" >&2
exit 0