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.↩