GlobalId Encoding
Ogólny zarys koncepcji znajduje się na blogu: "GlobalId encoding".
Fragmenty kodu z wyjaśnieniami
auth_code
Jednym z elementów PoC jest skrócenie wyjścia funkcji HMAC do możliwie krótkiego stringu, tak, by encoding identyfikatorów globalnych nie powodował bardzo dużego wzrostu objętości danych. Oczywiście wzrost i tak występuje, ale jest mniejszy, niż mógłby być. Wykorzystywany HMAC korzysta z algorytmu SHA1, który generuje wyjście postaci:
da39a3ee5e6b4b0d3255bfef95601890afd80709
W PoC ciąg ten skracany jest do 6 znaków, z czego 4 znaki pochodzą z wejściowego ciągu danych. Jest to realizowane przy pomocy funkcji auth_code (a właściwie __auth_code):
1 def auth_code(data, count):
2 pos = random.randint(0x00,0xff)
3 hashdata = __get_sha256(data)
4 return __auth_code(hashdata, count, pos)
Działanie funkcji auth_code jest proste - losuje ona wartość z zakresu od 0x00 do 0xff, i wywołuje właściwą funkcję __auth_code z parametrami, którymi jest:
- sha256 z przekazanej w wywołaniu wartości,
- wylosowana wartość,
Uwaga:
Można zastąpić funkcję sha256 przez sha512. Funkcje te zwracają odpowiednio ciągi (w postaci HEX) o długości 64 i 128 znaków. Choć wartość pos przyjmuje 256 możliwych wartości, rzeczywista ilość różnych "skrótów skrótu" ograniczona jest do długości hashdata (tylko tyle jest możliwych pozycji startowych), tak więc im hashdata jest dłuższe, tym lepiej. -- PawelGolen 2009-01-29 07:03:41
1 def __auth_code(data, count, pos):
2 i = 0
3 auth = "%02x" % pos
4 pos = pos % len(data)
5 while i < count:
6 auth += data[pos]
7 while data[pos] == "0":
8 pos = (pos + 1) % len(data)
9 pos = (pos + int(data[pos], 16)) % len(data)
10 i += 1
11 return auth
Funkcja __auth_code dokonuje właściwego skracania sha256 do pożądanej długości. Generowanie skrótu odbywa się poprzez odczytanie wartości znaków w sha256 na określonych pozycjach. Pozycje określane są w sposób następujący:
pozycja startowa określana jest przez parametr pos,
kolejna pozycja to obecna pozycja plus wartość znaku na obecnej pozycji,
Operacja ta jest powtarzana tyle razy, ile wskazuje parametr count, przy czym w pozostałej części PoC wykorzystana jest wartość 4. W przypadku, gdy na danej pozycji znajduje się 0, wartość ta jest wykorzystywana (dodawana) do wyjścia, ale o przesunięciu decyduje wartość następnego znaku, który nie jest 0. Ostatecznie otrzymywany jest ciąg postaci:
e3dc5b
Maksymalna możliwa ilość wyjść funkcji auth_code dla jednej wartości parametru data to 256, czyli tyle, ile jest możliwych wartości losowanego w niej parametru.
Weryfikacja auth_code odbywa się za pomocą funkcji auth_verify:
1 def auth_verify(data, auth):
2 pos = int(auth[:2],16)
3 hashdata = __get_sha256(data)
4 auth_tmp = __auth_code(hashdata, len(auth) - 2, pos)
5 return auth_tmp == auth
Funkcja na wejściu otrzymuje dane (data) oraz wartość auth. Porównywana jest wartość auth_code przekazana w parametrze, z wartością wyliczoną przy pomocy funkcji __auth_code.
Prawdopodobieństwo zaistnienia zdarzenia "kolizji" wynosi 1/16^4. Ilość elementów, które generują taki sam kod auth_code zależy od mocy zbioru dopuszczalnych elementów. Dla przykładu w zakresie liczb od 1 do 1000000 będzie około 16 różnych wartości generujących ten sam auth_code.
ShortSecureGlobalId
Klasa ShortSecureGlobalId udostępnia dwie funkcje:
- put - funkcja zwraca zakodowany identyfikator,
- get - funkcja dekoduje zakodowany identyfikator,
ShortSecureGlobalId.__init__
W konstruktorze ustawianych jest kilka unikalnych wartości dla każdej instancji.
1 def __init__(self):
2 self.__keySize = 512
3 self.__authSize = 4
4 self.__authKey = os.urandom(self.__keySize)
5 self.__encKey = os.urandom(self.__keySize)
6 self.__encCookie = struct.pack("B", random.randint(0,0xFF))
7 self.__digestmod = hashlib.sha1
__keySize - rozmiar kluczy auth/key
__authSize - rozmiar,
__authKey - losowy klucz o rozmiarze keySize,
__encKey - losowy klucz o rozmiarze keySize,
__encCookie - losowa wartość "cookie",
ShortSecureGlobalId.put
1 def put(self, raw_id):
2 """
3 Zwraca zakodowany identyfikator globalny przekazany w raw_id
4 """
5 auth_part = auth_code(self.__getHMAC(self.__addEncCookie(raw_id)), \
self.__authSize)
6 enc_part = xor_crypt(self.__getEncKey(auth_part), \
to_hex(self.__addEncCookie(raw_id) ))
7 enc_id = "%s%s" % (auth_part, enc_part)
8 return enc_id
Rezultat funkcji put składa się z części:
- auth_part,
- enc_part,
Część auth_part jest rezultatem wywołania opisywanej już funkcji auth_code z parametrami:
rezultat HMAC na identyfikatorze z wykorzystaniem klucza __authKey,
określenie długości wyjścia, w tej chwili 4 (__authSize),
Wykorzystanie HMAC powoduje, że dla tej samej wartości identyfikatora dla różnych sesji, rezultat auth_code będzie różny, bo różne są dane wejściowe.
Część enc_part jest rezultatem operacji XOR, gdzie klucz szyfrowania jest otrzymywany przez wywołanie funkcji __getEncKey i oparty jest na operacji HMAC na auth_part i kluczu __encKey. Dane szyfrowane to identyfikator oraz jeden bajt kontrolny o wartości unikalnej dla każdej sesji (__encCookie).
UWAGA:
Wykorzystanie XOR wynika niejawnie z założenia, że poufność identyfikatora nie jest istotna. Co prawda klucz szyfrowania nie jest stały, ale istnieje 256 możliwych wartości zakodowanego identyfikatora. Oznacza to, że te same dane (wartość identyfikatora) szyfrowane są różnymi kluczami. 256 próbek wystarcza, by określić jaką rzeczywistą wartość ma zakodowany identyfikator. Przykład: Jak nie używać XOR. -- PawelGolen 2009-01-29 07:12:48
Przykładowe rezultaty funkcji put dla identyfikatora o wartości 1:
918c37cffa e194f3407b 4933970db5 f1c356a51e 2ef3568287 6c4d358e14 72c175df52 13fd1d5206 924d949447 c3c9743c88
ShortSecureGlobalId.get
Funkcja get jest w zasadzie odwrotnością funkcji put:
1 def get(self, enc_id):
2 """
3 Odkodowuje identyfikator globalny i zwraca raw_id
4 """
5 auth_part = enc_id[:2 + self.__authSize]
6 enc_part = enc_id[2 + self.__authSize:]
7 dec_data = from_hex(xor_crypt(self.__getEncKey(auth_part), enc_part))
8 raw_id, encCookie = self.__splitIdAndEncCookie(dec_data)
9 # Sprawdź, czy wartość auth_auth part dla odszyfrowanych danych zgadza
10 # się z częścią przekazaną jawnie. Jeśli nie, zgłoś wyjątek
11 if not auth_verify(self.__getHMAC(self.__addEncCookie(raw_id)), auth_part):
12 raise DataIntegrityException()
13 # Sprawdż, czy encCookie z odszyfrowanych danych zgadza się z
14 # self.__encCookie. Jeśli nie, zgłoś wyjątek
15 if not encCookie == self.__encCookie:
16 raise DataIntegrityException()
17 return raw_id
Na początu otrzymany zakodowany identyfikator jest dzielony na dwie części:
- auth_part,
- enc_part,
Na podstawie auth_part generowany jest klucz szyfrowania i enc_part jest odszyfrowane do postaci dec_part. Część dec_part jest dalej dzielona na dwie części:
- raw_id
- encCookie
Dodatkowo wykonywane są dwa testy:
czy auth_code dla dec_part zgadza się z częścią auth_part,
czy odszyfrowane encCookie zgadza się z __encCookie,
Jeśli którykolwiek z tych testów zakończy się niepowodzeniem, zgłaszany jest wyjątek.
Wyzwanie
Cały PoC nie ma za zadanie stworzenia metody kodowania identyfikatorów globalnych w taki sposób, by ich użycie było całkowicie bezpieczne. Testy "integralności" otrzymanych danych w szczególności nie mogą zastąpić dobrze zaimplementowanej kontroli dostępu. PoC pokazuje dodatkową linię ochrony, która może być przydatna, jeśli:
- trzeba szybko poprawić aplikację, która nie ma poprawnej kontroli dostępu,
- istnieją błędy w implementacji kontroli dostępu,
Utrudnienie polega na tym, że atakujący musi znaleźć odpowiednią wartość zakodowanego identyfikatora, która po rozkodowaniu po stronie serwera spełni warunki:
- identyfikator będzie miał pożądaną wartość,
- przejdzie kontrolę integralności złożoną z:
weryfikacją skróconej wersji HMAC,
weryfikacją encCookie,
WYZWANIE: Znaleźć metodę generowania prawidłowych wartości zakodowanych identyfikatorów, o ile jest to możliwe.
Pełny kod PoC
1 # -*- coding: utf8 -*-
2
3 import os
4 import hashlib
5 import hmac
6 import gzip
7 import binascii
8 import random
9 import struct
10
11 def __get_sha256(data):
12 sha256 = hashlib.new("sha256")
13 sha256.update(data)
14 hashdata = sha256.hexdigest()
15 del sha256
16 return hashdata
17
18
19 def auth_code(data, count):
20 pos = random.randint(0x00,0xff)
21 hashdata = __get_sha256(data)
22 return __auth_code(hashdata, count, pos)
23
24
25 def __auth_code(data, count, pos):
26 i = 0
27 auth = "%02x" % pos
28 pos = pos % len(data)
29 while i < count:
30 auth += data[pos]
31 while data[pos] == "0":
32 pos = (pos + 1) % len(data)
33 pos = (pos + int(data[pos], 16)) % len(data)
34 i += 1
35 return auth
36
37
38 def auth_verify(data, auth):
39 pos = int(auth[:2],16)
40 hashdata = __get_sha256(data)
41 auth_tmp = __auth_code(hashdata, len(auth) - 2, pos)
42 return auth_tmp == auth
43
44
45 def xor_crypt(key, data):
46 """
47 XOR na dwóch blokach HEX klucz musi być krótszy, lub równy długości data
48 """
49 assert len(data) <= len(key)
50 out = ""
51 for pos in xrange(0, len(data),2):
52 out += "%02x" % ( int(key[pos:pos+2], 16) ^ int(data[pos:pos+2], 16))
53 return out
54
55
56 to_hex = binascii.hexlify
57 from_hex = binascii.unhexlify
58
59 class ShortSecureGlobalId:
60 """
61 PoC bezpiecznych identyfikatorów globalnych.
62
63 Dla każdej instancji klasy generowane są dwa unikalne klucze:
64 __authKey - użyty do generowania HMAC dla identyfikatora,
65 __encKey - użyty do generowania klucza szyfrowania,
66
67 Encodowany identyfikator składa się z:
68 - 1: losowego bajtu z zakresu 0x00 do 0xFF,
69 - 2: wartości zależnej od 1 oraz HMAC dla identyfikatora o długości __authSize,
70 - 3: zakodowanej wartości identyfikatora
71 Całość reprezentowana jest jako HEX. Przykład:
72 e4a7dc53aa7113:
73 e4 - 1
74 a7dc - 2
75 53aa7113 - 3
76
77 W skład 3 wchodzi:
78 - zakodowany identyfikator
79 - zakodowane __encCookie
80
81 Kodowanie odbywa się przy pomocy operacji XOR z wykorzystaniem klucza __encKey.
82 """
83
84 def __init__(self):
85 self.__keySize = 512
86 self.__authSize = 4
87 self.__authKey = os.urandom(self.__keySize)
88 self.__encKey = os.urandom(self.__keySize)
89 self.__encCookie = struct.pack("B", random.randint(0,0xFF))
90 self.__digestmod = hashlib.sha1
91
92
93 def __getHMAC(self, data):
94 """
95 Zwraca hmac (hex) z podanych danych
96
97 HMAC jest wykonywany na przekazanych danych z wykorzystaniem:
98 - klucza self.__authKey ustalanego w __init__
99 - funkcji skrótu wskazywanej przez self.__digestmod
100 """
101 mac = hmac.new(self.__authKey, digestmod = self.__digestmod)
102 mac.update(data)
103 return mac.hexdigest()
104
105
106 def __getEncKey(self, seed):
107 """
108 Zwraca klucz szyfrowania (sha512) dla podanego seeda
109
110 Klucz jest tworzony jako HMAC korzystający z sha512 i self.__encKey
111 """
112 key = hmac.new(self.__encKey, digestmod = hashlib.sha512)
113 key.update(seed)
114 return key.hexdigest()
115
116
117 def __addEncCookie(self, raw_id):
118 """
119 Dodaje __encCookie do raw_id
120 """
121 return "%s%s" % (raw_id, self.__encCookie)
122
123
124 def __splitIdAndEncCookie(self, dec_data):
125 """
126 Rozdziela encCookie i raw_id
127 """
128 return (dec_data[:-len(self.__encCookie)], \
dec_data[-len(self.__encCookie):])
129
130
131 def put(self, raw_id):
132 """
133 Zwraca zakodowany identyfikator globalny przekazany w raw_id
134 """
135 auth_part = auth_code(self.__getHMAC(self.__addEncCookie(raw_id)), \
self.__authSize)
136 enc_part = xor_crypt(self.__getEncKey(auth_part), \
to_hex(self.__addEncCookie(raw_id) ))
137 enc_id = "%s%s" % (auth_part, enc_part)
138 return enc_id
139
140
141 def get(self, enc_id):
142 """
143 Odkodowuje identyfikator globalny i zwraca raw_id
144 """
145 auth_part = enc_id[:2 + self.__authSize]
146 enc_part = enc_id[2 + self.__authSize:]
147 dec_data = from_hex(xor_crypt(self.__getEncKey(auth_part), enc_part))
148 raw_id, encCookie = self.__splitIdAndEncCookie(dec_data)
149 # Sprawdź, czy wartość auth_auth part dla odszyfrowanych danych zgadza
150 # się z częścią przekazaną jawnie. Jeśli nie, zgłoś wyjątek
151 if not auth_verify(self.__getHMAC(self.__addEncCookie(raw_id)), auth_part):
152 raise DataIntegrityException()
153 # Sprawdż, czy encCookie z odszyfrowanych danych zgadza się z
154 # self.__encCookie. Jeśli nie, zgłoś wyjątek
155 if not encCookie == self.__encCookie:
156 raise DataIntegrityException()
157 return raw_id
158
159
160 class DataIntegrityException(Exception):
161
162 #pylint: disable-msg=W0231
163 def __init__(self):
164 pass