A one-time pad lényegében egy stream alapú titkosÃtást valósÃt meg, ahol a bemenetet bitenként változtatjuk. Ugyan a kulcs ismeretének hiányában az adat nem állÃtható vissza veszteségmentesen. Ez azonban nem azt jelenti, hogy mindenféle támadás ellen védett lenne. A legnagyobb veszély a titkosÃtott üzenet manipulációja. Nézzünk erre egy példát.
Tételezzük fel, hogy az üzenet, amit küldeni akarunk az a "kerlek utalj 20000 Ft-ot". Ezt először byte sorozattá kell konvertálnunk. Ennek az eredménye: 6B 65 72 6C 65 6B 20 75 74 61 6C 6A 20 32 30 30 30 30 20 46 74 2D 6F 74
Majd generálunk egy véletlen kulcsot azonos hosszal és titkosÃtjuk a bemenetet. Ennek eredménye mondjuk1 ez lesz: ea c8 02 30 e4 1d a2 13 0f 1f a4 ae 6a e0 f8 22 35 da 9c 33 3a da 4f 60
Tételezzük fel, hogy ezt az üzenetet a támadó elkapja és az e0 byteot kicseréli eb-re. Ugyan nem tudja, hogy mit módosÃtott a kulcs hiányában, ennek ellenére sikeresen beavatkozott a kommunikációba és a fogadó oldalon a következÅ‘ üzenet érkezik: "kerlek utalj 90000 Ft-ot", vagyis az üzenet ép jelentéssel rendelkezhet, de mégis megváltozott.
Az ilyen támadásokat a fogadó oldalon extra adat nélkül lehetetlen detektálni. KézenfekvÅ‘ ötlet lenne, hogy egy hash értéket még küldjünk el az üzenettel, amibÅ‘l a fogadó fel tudja ismerni, hogy az üzenetet módosÃtották. A probléma ezzel a megoldással az, hogy ha csak sima hash kódot küldünk az adat mellé, akkor azt ugyanolyan könnyen módosÃthatja a támadó, hiszen csak annyit kell tennie, hogy újra kiszámolja a módosÃtott üzenethez tartozó hash értéket.
Azonban ha egy hash értéket alkalmazunk, amit az üzenetből és egy véletlenszerűen választott kulcsból képezünk, akkor már nehezebb dolga van a támadónak és megalkottunk egy kezdetleges HMAC (Hash Message Authentication Code) algoritmust.
A HMAC a MAC algoritmusok családjába tartozó algoritmus. A MAC algoritmusok hasonló tulajdonságokkal rendelkeznek, mint a kriptográfiai hash-függvények, de más biztonsági követelmények vonatkoznak rájuk. Ahhoz, hogy egy MAC biztonságosnak tekinthetÅ‘ legyen, ellen kell állnia az egzisztenciális hamisÃtásnak2 és a választott üzenet alapú támadásoknak.
Ez azt jelenti, hogy ha a támadónak hozzáférése van a titkos kulcshoz és tud MAC összegeket gyártani az általa választott üzenetekhez, akkor sem tudja kitalálni a MAC összegeket más üzenetekhez, még akkor sem, ha végtelen mennyiségű számÃtást végez.
A MAC abban különbözik a késÅ‘bbiek során tárgyalt digitális aláÃrástól, hogy a MAC értékek generálása és az ellenÅ‘rzése ugyanazzal a titkos kulccsal történik. Ez azt jelenti, hogy az üzenet küldÅ‘jének és fogadójának ugyanabban a kulcsban kell megegyeznie a kommunikáció elÅ‘tt, mint bármelyik más szimmetrikus titkosÃtás esetén. EbbÅ‘l kifolyólag a MAC megoldások nem biztosÃtják a digitális aláÃrás által kÃnált letagadhatatlanság tulajdonságát.
A bevezetÅ‘ben emlÃtett HMAC megoldás azért kezdetleges, mert a manapság használt kriptográfiai hash függvények rendelkeznek egy belsÅ‘ állapottal, ami ilyen MAC alkalmazásban bizonyos feltételek mellett kitalálható és módosÃtható, ami végsÅ‘ soron lehetÅ‘vé teszi azt, hogy az üzenethez még tetszÅ‘leges adatot hozzáfűzzünk.
A valódi HMAC megoldásokban éppen ezért két hash értéket használnak, ami ellehetetlenÃti a fentebb emlÃtett támadási módszert. A HMAC rendszerekben a megadott k kulcsból k1 és k2 alkulcsot képzünk. Az üzenetbÅ‘l elÅ‘ször hash értéket k1 kulcs segÃtségével gyártunk, majd a kapott értékbÅ‘l k2 segÃtségével újra létrehozunk egy hash értéket.
.NET használat
.NET esetén a különbözÅ‘ HMAC algoritmusok a HMAC osztályból öröklÅ‘dnek. Az osztály rendelkezik egy Create metódussal, aminek paraméterként a használni kÃvánt hash algoritmus nevét kell megadni.
A példányosÃtását leegyszerűsÃti az alábbi metódus, ami a korábban már használt HashAlgorithmName alapján példányosÃt:
private static HMAC Create(HashAlgorithmName name)
{
if (name == HashAlgorithmName.MD5)
return new HMACMD5();
else if (name == HashAlgorithmName.SHA1)
return new HMACSHA1();
else if (name == HashAlgorithmName.SHA256)
return new HMACSHA256();
else if (name == HashAlgorithmName.SHA384)
return new HMACSHA384();
else if (name == HashAlgorithmName.SHA512)
return new HMACSHA512();
else if (name == HashAlgorithmName.SHA3_256)
return new HMACSHA3_256();
else if (name == HashAlgorithmName.SHA3_384)
return new HMACSHA3_384();
else if (name == HashAlgorithmName.SHA3_512)
return new HMACSHA3_512();
else
throw new ArgumentException("Invalid hash algorithm", nameof(name));
}
Az aláÃrás példányosÃtás után a hmac példányon hÃvott ComputeHash hÃvással történik. Ez egy byte tömbben adja vissza a számÃtott értéket. A ComputeHash hÃvás elÅ‘tt a hmac példányon be kell állÃtanunk a kulcsot, amit byte tÃpusú tömbként kell megadnunk.
A legjobb eredmények biztonság tekintetében akkor érhetőek el, ha a kulcs véletlenszerűen generált és legalább olyan hosszú, mint a használt hash algoritmus kimeneti bitjeinek száma.
A ComputeHash rendelkezik egy aszinkron változattal is, ami a ComputeHashAsync nevet kapta. Az alábbi extension metódus egy tetszÅ‘leges Stream tÃpusú bemenetet Ãr alá:
public static async Task Sign(this Stream input,
byte[] key,
HashAlgorithmName hashAlgorithm,
Stream output,
IProgress<float>? progress = null,
CancellationToken cancellationToken = default)
{
long position = 0;
long size = input.Length;
int read = 0;
using (HMAC hmac = Create(hashAlgorithm))
{
hmac.Key = key;
byte[] hash = await hmac.ComputeHashAsync(input, cancellationToken);
input.Seek(0, SeekOrigin.Begin);
await output.WriteAsync(hash, 0, hash.Length);
byte[] buffer = new byte[BufferSize];
do
{
cancellationToken.ThrowIfCancellationRequested();
read = await input.ReadAsync(buffer, 0, buffer.Length, cancellationToken);
await output.WriteAsync(buffer, 0, read, cancellationToken);
position += read;
progress?.Report((float)position / size);
}
while (read > 0);
}
}
A metódusban a számÃtott aláÃrás a kimeneti stream elejére kerül, majd a bemeneti stream ezt követÅ‘en a kimenetbe másolódik.
A visszaellenÅ‘rzés folyamata hasonló az aláÃráshoz. A kapott stream-re ki kell számolni ugyanúgy a HMAC értéket a kulcs segÃtségével, majd ha kapott két byte tömböt kell összehasonlÃtani.
Az ellenÅ‘rzésre az alábbi extension metódust készÃtettem:
public static async Task<bool> Verify(this Stream input,
byte[] key,
HashAlgorithmName hashAlgorithm,
CancellationToken cancellationToken = default)
{
int read = 0;
using (HMAC hmac = Create(hashAlgorithm))
{
hmac.Key = key;
byte[] storedHmac = new byte[hmac.HashSize / 8];
read = input.Read(storedHmac, 0, storedHmac.Length);
if (read != storedHmac.Length)
return false;
byte[] computed = await hmac.ComputeHashAsync(input, cancellationToken);
for (int i=0; i<storedHmac.Length; i++)
{
if (computed[i] != storedHmac[i])
{
return false;
}
}
return true;
}
}
A turpisság annyi, hogy ha a fenti Sign metódussal generáltattuk az aláÃrást, akkor a bemeneti stream elejérÅ‘l ki kell olvasni az átküldött HMAC értéket. A hmac példány HashSize tulajdonsága a használt algoritmus bitméretét adja vissza, amit ha elosztunk nyolccal, akkor megkapjuk, hogy pontosan mennyi byte-ot kell a stream elejébÅ‘l kihagynunk a HMAC számÃtás során.
-
A példában használt kulcs:
81 ad 70 5c 81 76 82 66 7b 7e c8 c4 4a d2 c8 12 05 ea bc 75 4e f7 20 14↩ -
Az egzisztenciális hamisÃtás gyenge üzenetekkel kapcsolatos hamisÃtás a kriptográfiai digitális aláÃrási sémával szemben. Akkor beszélhetünk egzisztenciális hamisÃtásról, ha adott az áldozat ellenÅ‘rzÅ‘ kulcsa és a támadó legalább egy
múj üzenethezsaláÃrást talál úgy, hogy azsaláÃrás érvényesm-re az áldozat ellenÅ‘rzÅ‘ kulcsához képest. Az üzenetnek nem kell értelmesnek vagy semmilyen módon hasznosnak lennie. Az egzisztenciális hamisÃtás a támadás kimenetelét határozza meg, nem pedig azt, hogy a támadó hogyan vagy milyen gyakran tud interakcióba lépni a megtámadott aláÃróval a támadás során.↩