Kolega poprosił mnie abym napisał coś krótkiego związanego z bezpieczeństwem IT. Szczerze mówiąc nie miałem ani weny ani pomysłu. Odmówiłem wykręcając się brakiem czasu, zwłaszcza że miałem nową zabawkę - elektroniczną Enigmę do zlutowania. Jednakże zgłębiając tematykę i historię łamania szyfrogramów Enigmy pomyślałem, że może dałoby się te dwa tematy jakoś luźno połączyć. W ten sposób pół żartem, pół serio napisałem instrukcję implementacji mechanizmu dwu-etapowego uwierzytelniania (MFA) w OpenVPN-ie z wykorzystaniem szyfrowania Enigmy i prawdziwej niemieckiej książki kodów. Więcej na temat samej Enigmy (tej prawdziwej jak i elektronicznej), zasady działania, ciekawostek - znajdziesz tu: https://enigma.3d.pl/opis.

Let's go - dodajmy MFA do OpenVPN!

 

rys. 1 moja elektroniczna Enigma

 

Jakiś czas temu wdrażałem nową bramkę OpenVPN i pamiętam, że nie było wtedy gotowego mechanizmu MFA dostępnego dla OpenVPN. Aby dodać dwu-etapowe uwierzytelnianie napisałem swój skrypt uwierzytelniający i podpiąłem go po stronie serwera (auth-user-pass-verify).

 

Jeśli w configu OpenVPN, po stronie klienta, dopiszesz dyrektywę: auth-user-pass spowoduje to, że OpenVPN jeszcze przed połączeniem wyświetli monit o podanie użytkownika i hasła - patrz rys.2. Hasło to (oraz login) możemy wykorzystać właśnie w celu przeprowadzenia dodatkowego uwierzytelnienia (MFA).

 

rys.2 OpenVPN - okienko dialogowe z możliwością wprowadzenia użytkownika i hasła

 

OpenVPN w pierwszej kolejności sprawdzi podstawowe metody zabezpieczeń - tj. zgodność klucza ta.key, datę ważności i prawidłowość certyfikatu użytkownika (czy został podpisany przez nasze zaufane CA, czy nie został odwołany i umieszczony na liście CRL itd.). Dopiero gdy wszystkie zabezpieczenia w warstwie TLS powiodą się,  wówczas klient prześle do serwera wprowadzone przez użytkownika parametry. Aby serwer mógł zweryfikować wprowadzone dane (tj. użytkownika i hasło), należy w jego pliku konfiguracyjnym dodać linię :

auth-user-pass-verify /etc/openvpn/server/mfa_helper.bash via-file
 

gdzie /etc/openvpn/server/mfa_helper.bash to skrypt weryfikujący wprowadzone dane. Serwer OpenVPN wywoła wówczas wskazany skrypt i przekaże do niego ścieżkę tymczasowego pliku, w którym zapisane będą wprowadzone przez użytkownika dane (credentiale).

/etc/openvpn/server/mfa_helper.bash /sciezka/do/tymczasowego/pliku/z/credentialami

 

Skrypt należy napisać samemu, gdyż twórcy OpenVPN-a pozostawiają nam tutaj zupełną dowolność co do metod weryfikacji wprowadzonych danych jak i użytych do tego narzędzi. Skrypt możesz napisać w bashu, perlu, pythonie - czymkolwiek. Nazwa użytkownika oraz hasło zapisane są w pliku tymczasowym odpowiednio w pierwszej i drugiej linii. Przykładowy - najprostszy skrypt napisany w bashu pokazałem na listingu 1.

 

#!/bin/bash

if [ "$#" -ne 1 ] ; then
    echo "bad syntax"
    exit 1
fi

# odczytujemy uzytkownika i hasło z przekazanego jako parametr pliku tymaczasowego
readarray -t lines < $1
username=${lines[0]}
password=${lines[1]}

if [[ "$password" == "mySeCrEtPaSs" ]]; then
  #echo "ok"
  exit 0
fi

#echo "bad password"
exit 1

## eof ##########################################################

listing 1. Najprostszy skrypt weryfikujący wprowadzone dane.

 

Dla OpenVPN-a ważny jest tak naprawdę kod z jakim skrypt zakończył działanie. Jeśli skrypt zakończy działanie z kodem 0 oznacza to, że uwierzytelnienie powiodło się i OpenVPN umożliwi zestawienie tunelu. Jeśli zakończymy skrypt kodem 1 (błąd) wówczas połączenie zostanie przerwane!. Nie musisz martwić się o uprawnienia jak i kasowanie pliku tymczasowego - OpenVPN sam o to dba.



Tyle teorii odnośnie sposobu w jaki OpenVPN umożliwia dodatkową weryfikację wprowadzonych przez użytownika danych. Tak naprawdę w tym momencie mógłbym zakończyć ten tutorial. Wybór dalszej implementacji zależy bowiem od środowiska w firmie, inwencji admina czy przyjętej polityki. Możesz tutaj podpiąć skrypt odpytujący serwer radiusa. Możesz pokusić się o uwierzytelnienie przez Active Directory. Możesz także dodać dość popularny mechanizm TOTP (Time-based one-time password). Świetnie nada się w tym celu biblioteka do Pythona pyotp. Być może zbiorę się w sobie niebawem i opiszę dokładnie mechanizm TOTP jako MFA do OpenVPN-a. Tymczasem miało być o Enigmie!

 

MFA z wykorzystaniem szyfrów Enigmy

 

W tej części zakładam, że czytelnik zna podstawy działania maszyny Enigma i wie czym są pojęcia Grundstellung (ustawienie wirników), Ringstellung (ustawienia pierścieni) czy też Steckerbrett verbindungen (połączenia na krosownicy kablowej). Starałem się opisać wszystkie te najważniejsze terminy prosto i konkretnie.


Po stronie serwera będziemy potrzebowali programu aenig4. Jest to konsolowy emulator Enigmy M4. Po rozpakowaniu ZIP-a program należy skompilować (jest też gotowa binarka pod Windows). Upewnij się, że w swojej dystrybucji linuksa masz zainstalowany kompilator gcc, program make itd. W przypadku Debiana i jego pochodnych mam tutaj na myśli pakiet build-essential. Upewnij się też czy masz zainstalowany pakiet help2man i w razie potrzeby doinstaluj (sudo apt-get install help2man). Na listingu 2 przedstawiłem proces kompilacji i instalacji.

 

root@msdev:/src/aenig4-master# ./bootstrap

autoreconf: export WARNINGS=
autoreconf: Entering directory '.'
autoreconf: configure.ac: not using Gettext
autoreconf: running: aclocal
autoreconf: configure.ac: tracing
autoreconf: configure.ac: not using Libtool
autoreconf: configure.ac: not using Intltool
autoreconf: configure.ac: not using Gtkdoc
autoreconf: running: /usr/bin/autoconf
autoreconf: running: /usr/bin/autoheader
autoreconf: running: automake --add-missing --copy --no-force
autoreconf: Leaving directory '.'
root@msdev:/src/aenig4-master# ./configure --prefix=/usr/local/aenig4 && make && make install

checking for a BSD-compatible install... /usr/bin/install -c
checking whether build environment is sane... yes
checking for a race-free mkdir -p... /usr/bin/mkdir -p
checking for gawk... no
checking for mawk... mawk
checking whether make sets $(MAKE)... yes
checking whether make supports nested variables... yes
checking for gcc... gcc
[ ... ]
[ ... ]
 /usr/bin/install -c -m 644 AUTHORS README NEWS COPYING '/usr/local/aenig4/share/doc/aenig4'
make[2]: Opuszczenie katalogu '/src/aenig4-master'

Listing 2. kompilacja programu aenig4

 

Po stronie klienta (użytkownika) posłużymy się emulatorem Enigmy pod Androida. Teraz musisz zastanowić się, czy ustawienia parametrów Enigmy (wirniki, pierścienie) będą generowane na podstawie jakichś zmiennych środowiskowych (data, czas, adres IP etc.) czy też na podstawie wcześniej wygenerowanej listy (odpowiednik niemieckiej książki kodów). Na potrzeby niniejszego artykułu przyjąłem, że ustawienia wirtualnej Enigmy będą brane z wcześniej wygenerowanej listy kodów - dokładnie jak miało to miejsce podczas II Wojny. Przygotowałem w tym celu specjalny generator ustawień Enigmy. Jednakże aby uczynić nasz przykład jeszcze bardziej realistyczny wykorzystamy tutaj istniejący niemiecki dokument - patrz rys.3.

 

rys. 3 - miesięczna lista kodów obowiązująca w październiku 1944 (CSV)

 

Zauważ, że powżysza lista przygotowana jest dla Enigmy M3. Na serwerze mamy jednak emulator Enigmy M4 (program aenig4). Będziemy musieli to uwzględnić, mianowicie w skrypcie na serwerze będziemy musieli ustawić czwarty wirnik w pozycji "A". Więcej na temat kompatybilności Enigmy M3 z M4 oraz innych ciekawostek znajdziesz w osbnych wpisach w menu z lewej strony ekranu. Jak widać na rysunku 3 nie mamy tutaj początkowych ustawień wirników (Grundstellung). Na potrzeby tego artykułu przyjąłem, że ustawienia wirników będą przedostatnią grupą kolumny Kenngruppen. Lista kodów z rys. 3 nie zawiera także informacji, którego reflektora (Umkehrwalze) należy użyć. W takim przypadku używano reflektora B i my także tak uczynimy. Zatem, dla 15-tego dnia ustawienia wyglądają następująco:

dzień ustawienia wirników
(Walzenlage)
ustawienia pierścieni
(Ringstellung)
reflektor
(UKW)
ustawienia łącznicy
(stecker verb.)
ustawienia początkowe
(Grundstellung)
parametry wywołania aenig4
15 III II V 06 16 02
F  P  B
UKW-B GT YC EJ UA RX PN IS WB MH ZV Y Z K aenig4 -k "b beta III II V 01 06 16 02 AYZK GT YC EJ UA RX PN IS WB MH ZV"

 

rys. 4 - konfigurowanie ustawień

 

rys. 5 - ustawienia początkowe

 

rys. 6 - zakodowana testowa wiadomość

 

Ustaw aplikacje w telefonie zgodnie z parametrami z rys. 4. Następnie wpisz na wirtualnej klawiatuże TESTOWAXWIADOMOSC. Teraz na serwerze VPN wywołaj polecenie
echo TESTOWAXWIADOMOSC | aenig4 -k "B beta III II V 01 06 16 02 AYZK GT YC EJ UA RX PN IS WB MH ZV" --filter


marek@MS:~$ echo TESTOWAXWIADOMOSC | aenig4 -k "B beta III II V 01 06 16 02  AYZK GT YC EJ UA RX PN IS WB MH ZV" --filter
UFJSCLSJUKWXGIPKK

 

Porównaj teraz wynik polecenia z tekstem jaki pojawił się w aplikacji na telefonie. Dla powyższych ustawień zakodowany tekst to: UFJSCLSJUKWXGIPKK. Jeśli ciągi znaków nie zgadzają się, musisz dokładnie przeanalizować każdy z parametrów. W szczególności łatwo o pomyłkę w łącznicy. Zauważ, że w przypadku aplikacji na telefon ustawienia pierścieni podajemy literami, a w linuksie cyframi. Tutaj także łatwo o pomyłkę.

 

Parser pliku CSV - generator ustawien Enigmy

 

Pozostało nam napisanie parsera książki kodów dla danego miesiąca. Zakładam tutaj, że książka kodów będzie dostępna w postaci pliku CSV na serwerze. OpenVPN podczas łączenia klienta musi przeanalizować (przeparsować) ten plik aby odczytać parametry Enigmy dla danego dnia. Następnie skrypt musi wywołać emulator Enigmy aby uzyskać zakodowaną wartość i porównać ją z hasłem przekazanym przez użytkownika. Poniżej przedstawiam wycinek pliku CSV dla listy kodów z rysunku 3.


17;IV I II;12 08 21;ME HX BF WY ZD TR FJ AG IL KQ;tak pjs kdh jvh
16;I II III;07 11 15;WZ AB MO TF RX SG QU VI YN EL;pzg evw wyt iye
15;III II V;06 16 02;GT YC EJ UA RX PN IS WB MH ZV;bhe xzm yzk evp

Listing 3. wycinek pliku schluesselTafel.csv

 

Na listingu 4. przedstawiłem przykładowy helper do OpenVPN-a. Skrypt napisany w bashu, parsuje plik CSV i porównuje zaszyfrowane hasło z ciągiem wprowadzonym przez użytkownika w kliencie OpenVPN.

#!/bin/bash

if [ "$#" -ne 1 ] ; then
    echo "bad syntax"
    exit 1
fi

# odczytujemy uzytkownika i hasło z przekazanego jako parametr pliku tymaczasowego
readarray -t lines < $1
providedUsername=${lines[0]}
providedPassword=${lines[1]}

LOGFILE='/etc/openvpn/server/logs/mfa.log'

#zmienna pomocnicza
ERROR=0 

egrep -q "^`date +%d`;" schluesselTafel.csv || exit 1
DAY=`date +%d`

Walzen=`egrep "^$DAY;" /etc/openvpn/server/schluesselTafel.csv |awk -F';' '{print $2}'`
Ringst=`egrep "^$DAY;" /etc/openvpn/server/schluesselTafel.csv |awk -F';' '{print "01 "$3}'`
Grundst=`egrep "^$DAY;" /etc/openvpn/server/schluesselTafel.csv |awk -F';' '{print $5}' |awk '{print "A" $3}' | tr [:lower:] [:upper:]`
Stecker=` egrep "^$DAY;" /etc/openvpn/server/schluesselTafel.csv |awk -F';' '{print $4}'`
ClearTextPass=`egrep "^$DAY;" /etc/openvpn/server/schluesselTafel.csv |awk -F';' '{print $5}' |awk -F' ' '{print $1$2}'| tr [:lower:] [:upper:] | tr -d ' '`


## sprawdzamy czy aenig4 nie zglosi bledu. Jesli tak, ustawiamy zmienna ERROR na wartosc 1
echo "$ClearTextPass" | /usr/local/aenig4/bin/aenig4 -k "B beta $Walzen $Ringst $Grundst $Stecker" --filter > /tmp/enigma_output.txt || ERROR=1

if [ $ERROR -eq 1 ] ; then
    echo "500 `date +%d-%m-%Y__%H:%M` => $providedUsername MFA - blad ustawien Enigmy: `cat /tmp/enigma_output.txt`" >> $LOGFILE
    exit 1
fi

Encrypted=`echo "$ClearTextPass" | /usr/local/aenig4/bin/aenig4 -k "B beta $Walzen $Ringst $Grundst $Stecker" --filter`

###echo "ClearTextPass: $ClearTextPass => $Encrypted" >> $LOGFILE

if [ $providedPassword == $Encrypted ] ; then
    echo "200 `date +%d-%m-%Y__%H:%M` => $providedUsername MFA authorization OK!" >> $LOGFILE
    exit 0
fi

## skoro stringi sie nie zgadzaja wychodzimy z kodem 1
echo "403 `date +%d-%m-%Y__%H:%M` => $providedUsername MFA authorization FAILED" >> $LOGFILE

exit 1

#### EOF ######################
 

Listing 4. Przykładowy helper dla OpenVPN-a napisany w bashu.

 

Pozostaje jeszcze kwestia hasła jakie podlegać będzie szyfrowaniu (co użytkownik ma wprowadzić jako tekst do zakodowania). Na potrzeby artykułu przyjąłem, że hasło będą stanowić dwa pierwsze bloki ostatniej kolumny (Kenngruppen) - pisane bez spacji i z wielkiej litery. W omawiannym przypadku będzie to BHEXZM. Możesz wymyśleć cokolwiek innego, ważne aby helper po stronie serwera "znał" to hasło, by móc je zaszyfrować i potem porównać z wprowadzonym przez użytkownika hasłem.

 

Dalszą część artykułu pisałem następnego dnia, tj. 16/07. Na rysunkach 7-10 przedstawiono zrzuty z aplikacji Androidowej dla tego dnia. Zakodowane hasło należy podać w kliencie OpenVPN.

 

rys. 7-10 ustawienia emulatora Enigmy

 

dzień ustawienia wirników
(Walzenlage)
ustawienia pierścieni
(Ringstellung)
reflektor
(UKW)
ustawienia łącznicy
(stecker verb.)
ustawienia początkowe
(Grundstellung)
parametry wywołania aenig4
16 I II III 07 11 15
G  K  O
UKW-B WZ AB MO TF RX SG QU VI YN EL W Y T aenig4 -k "B beta I II III 01 07 11 15 AWYT WZ AB MO TF RX SG QU VI YN EL"

 

 

Jeśli wprowadzone hasło będzie zgodne z ciągiem wygenerowanym na serwerze, wówczas skrypt na serwerze zakończy działanie z kodem 0 i połączenie VPN zestawi się. Gdyby się tak nie stało należałoby sprawdzić jakie hasło wygenerował serwer. W tym celu najprościej będzie zapisać je do pliku loga wraz z pozostałymi parametrami wirtualnej Enigmy. Być może w samym pliku CSV jest dzieś błąd i emulator Enigmy zgłasza błąd.


Podobny mechanizm weryfikacji dwu-etapowej możesz zastosować dla innych usług - np. serwera SSH. Wówczas możnaby wykorzystać któryś z istniejących modułów / rozszerzeń dla PAM (pam_script.so, pam_python.so etc.).