[DajSięPoznać#16] Hashcash

Wstęp

Problem dość powszechny przy budowaniu API: kiedy publikujemy endpointy niezabezpieczone autentykacją to narażamy się na potencjalne zagrożenia atakami DoS, działalność botnetu, usług SaaS (Spam as a service) i innych dziwactw powstałych po ciemnej stronie internetu.

Najprostszym rozwiązaniem tego problemu jest CAPTCHA, ale jej użycie niesie za sobą pewne konsekwencje. Z jednej strony wielu użytkowników będzie podirytowanych koniecznością rozwiązywania zagadek, przepisywania słabo widocznych tekstów itp, z drugiej strony istnieją serwisy oferujące rozwiazywanie CAPTCHY, np. antigate. Jeśli już o automatach mowa, to na ich temat można znaleźć ten przezabawny obrazek.

Proof-of-work

Podobnie jak w przypadku CAPTCHY mamy tu do czynienia z mechanizmem typu challengeresponse. Zamiast jednak angażować w cały proces użytkownika, wykorzystamy jego sprzęt, a konkretnie CPU.

Pomysł opiera się na teorii o nieodwracalności funkcji haszujących. Serwer udostępniający API przedstawia klientowi losową wiadomość – zagadkę lub w innych realizacjach klient sam ją sobie przygotowuje (np. przy wysyłaniu maili wiadomość musi zawierać adres adresata, co skutecznie odstrasza boty spamujące). Klient po otrzymaniu wiadomości ma za zadanie dokleić do niej ciąg znaków (przeważnie liczbę), tak, by skrót z całej nowej wiadomości spełniał określony warunek, na przykład pierwsze 3 znaki były zerami.

Algorytm wymaga nakładów obliczeniowych po stronie klienta i jest łatwo weryfikowalny po stronie serwera. Ze względu na nieprzewidywalność wyników działania funkcji haszującej, często stosowanym rozwiązaniem jest próbowanie zliczania haszy z wiadomości z doklejanymi kolejnymi liczbami naturalnymi – brute force, który prędzej czy później doprowadzi do rozwiązania,

A co z użytkownikiem ? Jeśli dobrze dobierzemy trudność zagadki, to trochę dłużej niż powinno pokręci mu się spinner przy oczekiwaniu na wyniki. I to wszystko, resztę zrobi za niego procesor.

Sam Hashcash przyjął się głównie jako źródło zabezpieczeń przed spamem emailowym, ale jest także używany w kryptowalutach takich jak bitcoin. Nic nie stoi na przeszkodzie, żeby zaimplementować własny algorytm proof of work, analogiczny do hashcasha,  którego zadaniem będzie opóźnianie wykonania kosztownych obliczeniowo serwisów (np. systemu rekomendacji).

Filtr WebAPI

Zadaniem filtru jest z jednej strony generowanie zagadek i przechowywanie ich w cache’u na krótki okres czasu, z drugiej strony weryfikacja, czy przysłana odpowiedź jest poprawna.

type ProofOfWorkFilterAttribute()  =
    inherit ActionFilterAttribute()

    let createChallenge() =        
        let rng = new RNGCryptoServiceProvider();
        let buffer = Array.zeroCreate<byte> 32
        rng.GetBytes(buffer);
        Convert.ToBase64String(buffer)    

    // ... pozostałe funkcje

    override x.OnActionExecuting(actionContext:HttpActionContext) =      
        let (found, values) = actionContext.Request.Headers.TryGetValues("X-Proof-Of-Work")
        if found = false then
            let challenge = createChallenge()
            storeChallenge challenge

            actionContext.Response <- new HttpResponseMessage()
            actionContext.Response.StatusCode <- HttpStatusCode.PreconditionFailed
            actionContext.Response.Content <- new StringContent(challenge)

        else
            let value = Seq.head values 
            let challenge = value |> fun value -> value.Split([|'#'|]) |> Seq.head

            if MemoryCache.Default.Contains(challenge) then
                let hash = value |> sha256
                if not(hash.StartsWith("0000")) then
                    actionContext.Response <- unauthorizedResponse()
            else
                actionContext.Response <- unauthorizedResponse() 
            
        ()

Użyty RNGCryptoServiceProvider gwarantuje znacznie większą nieprzewidywalność przy generowaniu losowych bitów.

Javascript

Z wykorzystaniem klienta Aurelii i biblioteki sjcl stworzonej przez uniwersytet Stanforda, może to wyglądać następująco:

client.get("api/recommendations" + q).then( res => {}, err => {
    if(err.statusCode === 412){
        let message = err.response;
        let i = 0;
        let candidate = "";

        while(true){
            candidate = message + "#" + i++;;
            let hashBits = window.sjcl.hash.sha256.hash(candidate);
            let hashStr = sjcl.codec.hex.fromBits(hashBits);
            if(hashStr.indexOf("0000") === 0){                        
                break;
            }
        }

        var client2 = new HttpClient().configure(x => {
                          x.withHeader('X-Proof-Of-Work', candidate);
                      });

        client2.get("api/recommendations" + q).then( resp => {
            self.results = JSON.parse(resp.response);
        });
    }
});
Advertisements

One thought on “[DajSięPoznać#16] Hashcash

  1. Pingback: [DajSięPoznać] Podsumowanie | When the smoke is going down

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s