Puncte:20

Cum pot înțelege dacă implementarea mea C este în timp constant sau nu (adică rezistentă la atacurile de sincronizare)

drapel jp

Am un cod pentru multiplicarea polinomilor și este scris în C. Am auzit că dacă o anumită instrucțiune este „timp constant” poate varia în funcție de arhitectură și de modelul procesorului și nu există nicio documentație oficială pentru acest comportament.Cum pot înțelege dacă codul meu este în timp constant sau nu?

Notă: Prin „Timp constant” mă refer la un software sau o bucată de cod rezistentă la atacurile de sincronizare. Folosesc UBUNTU pe un PC i7 de generația a 10-a

kelalaka avatar
drapel in
Compilează și vezi. Diferiți compilatori se comportă diferit și diferă și în arhitectura țintă. Puteți utiliza [Compiler Explorer](https://godbolt.org/) pentru a vedea ASM. De obicei este scris în ASM. [Urmăriți T1-ul nostru Thomas Pornin](https://www.youtube.com/watch?v=IHbtK5Kwt6A).
kelalaka avatar
drapel in
Concentrați-vă doar asupra părților cheie dependente. Rețineți că codul dvs. poate fi revizuit în codereview.se
esra avatar
drapel jp
@kelalaka înseamnă că trebuie să văd codul de asamblare pentru a mă asigura dacă codul este constant sau nu? Este necesara analiza cu asamblare? pot să o fac pe codul C de pe computerul meu?
esra avatar
drapel jp
@kelalaka codul meu nu este un protocol de criptare complet, ci doar un cod de multiplicare polinomială. Nu există chei exc. Pot măsura dacă codul meu de multiplicare este doar un timp constant sau nu?
kelalaka avatar
drapel in
Da, iar problema trebuie să vă asigurați că pentru fiecare compilație. Compiler Explorer este un instrument excelent pentru aceasta.
kelalaka avatar
drapel in
Întrebarea în atacul în timp este ce scăpați, dacă informațiile despre chei nu sunt scurse de ce să aveți timp constant?
esra avatar
drapel jp
@kelalaka scuze greșeala mea, folosesc o bucată de cheie în înmulțire. ai dreptate
kelalaka avatar
drapel in
[1](https://crypto.stackexchange.com/q/30958/18298), [2](https://crypto.stackexchange.com/q/82095/18298). [3](https://crypto.stackexchange.com/q/33252/18298), [4](https://crypto.stackexchange.com/q/80492/18298), [5](https:// crypto.stackexchange.com/q/75408/18298) și multe altele... vezi etichetele [tag:timing-attack], [tag:side-channel-attack]
kelalaka avatar
drapel in
Un duplicat pe mai multe site-uri de la infosec: [Cum pot preveni atacurile pe canale laterale împotriva autentificării?](https://security.stackexchange.com/q/220446/86735)
drapel nr
Nu înțeleg de ce naiba ar vrea cineva să încerce măcar să obțină cod în timp constant. După cum au menționat deja alții, este imposibil de garantat, chiar dacă remediați codul de asamblare, deoarece ceea ce face un CPU astăzi poate să nu fie ceea ce face următoarea generație a procesorului respectiv și nu există absolut nicio modalitate de a prezice. În loc să încercați să faceți ceva imposibil, adăugați pur și simplu o întârziere aleatorie care este cu un ordin de mărime mai mare decât timpul necesar codului dvs. Da, nu poate preveni cu totul atacurile de sincronizare, dar este mult mai bine decât să pierzi timp și energie încercând să egalezi timpii de execuție a CPU...
kelalaka avatar
drapel in
@user21820, în timp ce cercetătorii prezintă posibilitatea [atacurilor de sincronizare la distanță](http://crypto.stanford.edu/~dabo/papers/ssl-timing.pdf), există o mare îngrijorare că atacul de sincronizare poate expune secretul chei. O explicație simplă a fost [în acest răspuns](https://crypto.stackexchange.com/q/75408/18298). Nu vorbim despre securizarea a tot ceea ce vorbim despre securizarea părților dependente de cheie. Acesta este un punct de atac serios de la carduri inteligente la computere cloud partajate. Aceasta este criptografie unde diavolul în detalii și $$criptografie \neq criptomoneda$$
drapel nr
@kelalaka: Stai, nu am răspuns la comentariul tău, ci la ideea generală că se poate analiza codul de asamblare pentru a încerca să obțină o execuție în timp constant. Și nu văd de ce se pare că presupui că nu știu ce sunt atacurile de timp. Mai mult, chiar și comentariile la răspunsul tău legat repetă punctul pe care l-am spus aici.
kelalaka avatar
drapel in
@user21820 întregul punct de la primul meu comentariu, ASM. Rețineți că, Thomas Pornin a construit un compilator care este sigur în anumite ipoteze, desigur și acum încearcă să-l extindă în Turing complet, deși nu este necesar.
drapel nr
@kelalaka: Eh?? Nu am clarificat în mod explicit că asamblarea este **nu este suficientă** deoarece designul fizic al procesorului **nu** garantează nimic despre timpul de execuție? Nu are rost să vorbim în continuare despre modele abstracte atunci când securitatea se referă la **lumea reală**.
kelalaka avatar
drapel in
@user21820 nu este scris o singură dată compilat pentru fiecare țintă. Nu am spus niciodata asta. Trebuie să căutați toate modificările din procesor pentru a fi sigur că chiar și aceeași serie sunt sigure.... Majoritatea atacurilor au loc în lumea reală. Desigur, există atacuri academice care prevăd posibilitatea, cu toate acestea, aplicația reală poate deveni mult mai lungă, precum atacul de umplutură oracol; 2003 până în 2013. În această știință, uneori apare mai întâi atacul, apoi modelul și celălalt caz. Nu modelăm nici măcar lumea reală pentru a o analiza?
drapel nr
@kelalaka: Dacă spuneți că efectuați în mod repetat o analiză end-to-end pentru fiecare arhitectură CPU țintă pe care rulați codul, atunci da, sunt de acord că funcționează. Dar merită cu adevărat tot acest efort? Există multe modalități mai eficiente din punct de vedere al costurilor de a preveni atacurile de sincronizare decât să vă deranjați cu privire la codul de asamblare.
marshal craft avatar
drapel de
timp constant înseamnă că rulează într-un interval de timp limitat de o constantă. Deci, timpul de rulare a funcției cu orice intrare posibilă este întotdeauna mai mic decât o constantă. Deci, cum spunem, funcția revine întotdeauna în aproximativ 20 de secunde, indiferent de intrare, rulează în timp constant.
marshal craft avatar
drapel de
De asemenea, pentru algoritmul abstract de multiplicare polinomială, nu este timp constant. Timpul crește pe măsură ce adăugați factori sau introduceți polinoame de grad mai mare. Chiar nu ai pus o întrebare clară, deoarece există 2 cazuri. Funcția acceptă 2 intrări care sunt polinoame de diferite grade, practic lista de coeficienți având dimensiunea gradului intrării polinomului.
marshal craft avatar
drapel de
În acest caz simplu acceptând doar 2 intrări, numărul de înmulțiri și adunări crește în funcție de gradul sau numărul de coeficienți. Deci nu este legat de o constantă în general. Cu toate acestea, în realitate, în implementarea dvs., dacă există o limită maximă la intrare, atunci foarte simplu da, implementarea este în timp constant. Care ar fi cel mai rău caz pentru intrarea maximă. Deci, ca 2 grade polinomiale maxime, cu coeficienți valorați maxim ar reprezenta timpul constant. Doar pentru că implementarea dvs. utilizează resurse finite, definite de arhitectura și software-ul computerului.
Puncte:30
drapel ph

Din păcate, în absența documentației de la furnizorul CPU, nu puteți fi 100% sigur ce algoritmi vor fi sau nu constant. Acestea fiind spuse, cu siguranță există reguli de bază care pot fi folosite pentru a reduce riscul de probleme.

  1. Orice lucru care implică ramificarea fluxului de control sau execuția condiționată a codului este, evident, o problemă.
  2. Operațiile de comparare pot fi o problemă chiar dacă nu sunt utilizate direct pentru a afecta fluxul de control. Problema este că operatorii de comparație își stochează de obicei rezultatele într-un registru de steaguri și cea mai simplă modalitate de a obține un rezultat individual dintr-un registru de steaguri este adesea ramificarea sau execuția condiționată.
  3. Valorile booleene pot fi o problemă deoarece un compilator poate alege să înlocuiască o operație care implică un boolean cu execuție condiționată sau ramificare.
  4. Timpul pentru accesul la memorie poate varia în funcție de adresa accesată și de starea memoriei cache. Aceasta înseamnă că codul care implică tabele de căutare are un risc mare de a nu fi constant în timp.
  5. Înmulțirea și împărțirea pot fi implementate folosind algoritmi care necesită un număr variabil de cicluri pentru a se finaliza, multiplicarea este în general sigură pe procesoarele moderne „aplicații”, dar este posibil să nu fie pe microcontrolere. Nu aș considera diviziunea sigură pe niciun procesor.
  6. Deplasările și rotațiile de biți cu un număr variabil de poziții pot fi o problemă pentru unele procesoare, deoarece se pot traduce în bucle (fie în software, fie în hardware).
  7. Adunarea, scăderea, operațiile pe biți și deplasările de biți cu o constantă sunt în general OK.

Poate doriți să aruncați o privire la https://www.bearssl.org/constanttime.html care discută aceste probleme mai detaliat.

kelalaka avatar
drapel in
Rețineți că Squeamish Ossifrage are un răspuns extins pe Infosec [Cum pot preveni atacurile pe canale laterale împotriva autentificării?] (https://security.stackexchange.com/q/220446/86735). Asta acoperă totul în mai multe detalii.
drapel cn
Pe o platformă de 8 biți (card inteligent) am văzut că un compilator C a implementat incrementarea unui număr de 16 biți (adică, adăugarea 1) prin cele trei comenzi (1) incrementând octetul inferior, (2) salt condiționat următoarea instrucțiune dacă rezultatul este zero și (3) incrementând octetul superior.
Puncte:12
drapel in

Rețineți că acest răspuns excelent îi aparține Osifrage zguduitoare că au încetat contribuția! Am făcut un copy and paste apoi am făcut comunitate. Votarea acestui răspuns nu produce nimic pentru mine. Dacă puteți, votați-le Infosec.


Din exemplul de cod furnizat, este posibil să se determine parola corectă prin cronometrarea codului atunci când i se oferă diverse intrări.

În primul rând, nu ar trebui să examinați parola direct! La foarte putin, ar trebui să indexați parola cu un hash al parolei precum Argon2id și să comparați hash-ul parolei introduse cu hash-ul parolei pe care l-ați stocat în timpul înregistrării utilizatorului (sau când utilizatorul și-a schimbat ultima oară parola).

Și mai bine, ar trebui să utilizați un protocol de acord cu cheie autentificat cu parolă, cum ar fi OPAQUE, dar acestea pot depăși nivelul dvs. de plată în acest moment, până când vor fi adoptate și implementate pe scară largă.

Ce pot face pentru a mă asigura că codul meu nu este vulnerabil la astfel de atacuri de sincronizare?

Cel mai bun mod de a începe este să utilizați o rutină de bibliotecă sau o rutină primitivă altcineva a scris deja și are un motiv să mențină. De exemplu, în NaCl/libsodium, puteți utiliza crypto_verify_32 pentru a compara două șiruri de 32 de octeți, cum ar fi două coduri hash Argon2id sau două coduri de autentificare a mesajelor HMAC-SHA256. Apoi efortul de a răspunde la această întrebare poate fi concentrat pe un singur loc care va primi multă atenție și control și va ține pasul cu progresele.

Dar să zicem că nu ai crypto_verify_32, sau doriți să o implementați singur. Ce poti face?

Pentru a începe, trebuie să înțelegeți ce operațiuni au canale laterale. este ispititor a spune... cum au făcut și alte răspunsuri... că canalul lateral apare doar din cauza unei din timp avorta. Dar asta nu este toată povestea. În general, există multe operatii (aici scris în C pentru ilustrare) care poate dura o perioadă de timp care depinde de valorile a intrărilor... numim aceste operaţii timp variabil operațiuni, spre deosebire de timp constant*:

  • pentru (i = 0; i < n; i++) dacă (x[i] == y[i]) returnează EFAIL; evident ia mai puține iterații în buclă deci este practic garantat rularea in timp variabil in functie de valorile secrete ale x[i] și y[i].

  • Un simplu condițional dependent de secret pentru (i = 0; i < n; i++) dacă (x[i]) rău++;, dacă x[i] este secret, poate rula și în timp variabil chiar dacă bucla nu se întrerupe devreme. De ce?

    • Iată o aproximare grosieră. Instrucțiunile mașinii pe care procesorul le poate executa arată cam așa:

      0: tmp := x[i]
              ramificație la 1 dacă tmp este zero
              rău := rău + 1
      1: i := i + 1
              ramificație la 0 dacă i < n
      

      The numărul de instrucțiuni executat depinde de ce valoare a x[i] este la fiecare iterație: sărim rău := rău + 1 la unele iterații. Acesta este un model bun pentru modul în care timingul timpuriu atacă, de exemplu., RSA a funcționat ca în Lucrarea fundamentală a lui Kocher despre atacurile de sincronizare: bucla principală de exponențiere modulară calculează (să zicem) o pătrare modulară de 2048 de biți necondiționat, dar calculează o multiplicare modulară de 2048 de biți conditionat în funcţie de valoarea exponentului secret. Omiterea înmulțirii modifică substanțial timpul necesar întregii operațiuni.

    • Există, totuși, un alt motiv și are legătură cu predicție de ramură, un element cheie de design în ceea ce face ca CPU-urile moderne să ruleze atât de repede pe multe sarcini de lucru, chiar dacă scrieți aceeași cantitate de cod (să zicem, același număr de instrucțiuni ale mașinii și, într-un fel, garantați că acestea necesită același număr de cicluri pentru a calcula ) în fiecare ramură a unui condițional, timpul necesar pentru a se executa poate depinde de direcția în care a mers condiția.

    • În general, procesoarele sunt prost la păstrare care instrucțiuni au fost executate secret, așa că nu faceți alegerea instrucțiunilor depind de secrete.

  • Căutările în tabele/matrice pot dura o perioadă diferită de timp, în funcție de ce memorie a fost stocată în cache-ul CPU. În consecință, dacă locație în matrice din care citiți depinde de un secret, timpul necesar ar putea depinde de secret, care a fost exploatat pentru recuperați cheile AES prin sincronizarea cache.

    (Acest lucru face ca AES să fie un design destul de îndoielnic în retrospectivă, cu utilizarea sa intenționată a căutărilor de tabele dependente de cheie! NIST's justificare publicată (§3.6.2, Atacurile asupra implementărilor: rolul operaţiilor) susține în mod curios că căutările în tabele sunt „nu sunt vulnerabile la atacurile de sincronizare” în ciuda numeroaselor astfel de atacuri care au fost raportate de atunci.)

  • Schimbarea la distanță variabilă ca x = y << z poate dura mai mult timp pe unele procesoare dacă z este mai mare și mai puțin timp dacă este mai mic.

    (Asta face RC5 și finalista AES RC6 design destul de discutabil retrospectiv, cu utilizarea lor intenționată a distanțelor de rotație dependente de cheie!)

  • Pe unele procesoare, multiplicarea poate rula mai rapid sau mai lent, în funcție de dacă jumătatea superioară a intrărilor este zero sau nu.

  • Adăugarea de numere întregi pe 64 de biți pe procesoarele pe 32 de biți, în principiu, ar putea dura mai mult timp, în funcție de faptul dacă există o transferare. Asta pentru că, când X, y, și z, sunt numere întregi pe 64 de biți, logica x = y + z ar putea arăta mai mult ca:

    int carry = 0;
    x[0] = y[0] + z[0];
    dacă (adăugarea anterioară a debordat)
      purtare = 1;
    x[1] = y[1] + z[1] + purtare;
    

    În consecință, timpul necesar poate depinde de faptul dacă există o transferare de la suma jumătăților de 32 de biți inferioare la suma jumătăților de 32 de biți înalte. (În practică, aceasta este de obicei o preocupare doar pentru procesoarele exotice sau pentru alte tipuri de canale secundare, cum ar fi analiza puterii, care sunt relevante mai mult pentru cardurile inteligente decât pentru laptopuri și telefoane.)

Acest lucru ar putea suna puțin copleșitor. Ce putem face?

Există unele operații care în general fac rulează în timp constant pe majoritatea procesoarelor. Sunt:

  • Operații pe biți: X y, x | y, x ^ y, ~x, și altele care nu apar în C precum AND-cu-complement.
  • Constanta-distanta schimbari si rotatii precum schimbarea x << 3 sau rotațiax <<< 3 (nu C standard, dar comun în criptografie; înseamnă (x << 3) | (x >> (32 - 3)), dacă X este pe 32 de biți).
  • De multe ori adunarea și scăderea întregului: x + y, X y, când X și y sunt (să zicem) numere întregi nesemnate pe 32 de biți pe un procesor pe 32 de biți și adesea chiar numere întregi pe 64 de biți pe un procesor pe 32 de biți cu ajutorul instrucțiunilor ADD-with-carry.
  • Uneori inmultire intregi, dar povestea despre înmulțire este complicat, ceea ce este nefericit pentru criptografie, deoarece multiplicarea amestecă biți destul de frumos și are proprietăți algebrice utile.

Ca să fiu clar: nu vreau să spun asta un compilator C garantează rularea acestor operațiuni în timp constant dacă le utilizați într-un program C; Folosesc doar notația C pentru operațiile care CPU-uri se execută în general în timp constant. (Mai multe despre această avertizare într-un moment.)

„Dar stai,” protestezi, „cum pot să scriu un program util din aceste operațiuni? Fără condiționale? Fără bucle? Fără matrice?â

În primul rând, nu trebuie să evitați condiționalele, buclele sau matricele cu totul. Pur și simplu nu pot depind de secrete. De exemplu, pentru (i = 0; i < 32; i++) ... x[i] ... e bine. Dar pentru (i = 0; i < m[0]; i++) ... nu este bine dacă m[0] ar trebui să fie secret și pentru (i = 0; i < m[0]; i++) ... tab[x[i]] ... nu este bine dacă x[i] ar trebui să fie secret.

În al doilea rând, încă poți construi aceste lucruri! Doar că este puțin mai complicat. De exemplu, să presupunem b este un uint32_t care este fie 0, fie 1. Atunci b - 1 este fie -1 = 0xffffffff, fie, respectiv, 0, deci

x = ((b - 1) și z) | (~(b - 1) & y);

cauze x = y dacă b este 1 sau x = z dacă b este 0âmult asemănător x = (b ? y : z), dar fără ramură. Evident, acest lucru necesită calcul ambii y și z, deci există un anumit impact asupra performanței! În mod similar, puteți căuta un element al unui tabel privind în sus toate elemente ale tabelului și selectând-o pe cea dorită cu operații pe biți ca aceasta. Nu la fel de repede ca x[i], dar nici la fel de scurs.

În general, tu poate sa convertiți un program cu condiționale într-un circuit logic fără condiționale, chiar dacă nu o faceți vrei la din motive de performanţă. Există diverse alte trucuri similare pe care le puteți face. Puteți elabora o rutină de egalitate a memoriei în timp constant, cum ar fi crypto_verify_32 astfel, presupunând că x și y sunt matrice uint8_t:

uint32_t rezultat = 0;
pentru (i = 0; i < 32; i++)
  rezultat |= x[i] ^ y[i];
return ((rezultat - 1) >> 8) & 1;

Exercițiu: Acesta returnează 0 pentru egal și 1 pentru inegal, sau 0 pentru inegal și 1 pentru egal?

Scrierea unor programe ca acesta... și adoptarea unor criptosisteme precum X25519 a incuraja implementări care arată așa, în loc de criptosisteme precum RSA sau AES care a incuraja implementările care implică ramuri dependente de secret sau căutări de tabel dependente de secret – este un bun început pentru conectarea canalelor laterale de sincronizare.

Dar, există o captură! Îți amintești când am spus că compilatorul C nu garantează timp constant? Un compilator C inteligent precum Clang/LLVM ar putea recunoaşte că isteţul crypto_verify_32 bucla de mai sus poate fi executată mai eficient făcându-l să avorteze devreme și ar putea anula munca grea pe care ați făcut-o pentru a-l rescrie ca un circuit logic care rulează în timp constant. (În alte circumstanțe, s-ar putea să vă ajute, de exemplu, prin conversie x = (b ? y : z); într-o instrucțiune de mutare condiționată, CMOV, fără ramuri, dar de obicei nu vă puteți baza pe bunăvoința compilatorului C.)

Există câteva trucuri pe care le puteți face pentru a contracara acest lucru, cum ar fi un fragment de asamblare inline care face ca compilatorul să renunțe la aproximativ toate ipotezele pentru optimizare:

uint32_t rezultat = 0;
pentru (i = 0; i < 32; i++)
  rezultat |= x[i] ^ y[i];
asm volatile ("" ::: "memorie");
return ((rezultat - 1) >> 8) & 1;

Acest lucru poate sau nu să funcționeze cu compilatorul dvs. Pentru a fi încrezător, trebuie să examinați codul mașină generat de compilator... și chiar și atunci, un compilator ar putea efectua optimizări la timp care rescrie codul mașinii conform analizei de profilare, în special în limbaje de nivel superior precum Java. Deci s-ar putea să vrei cu adevărat scrieți logica în asamblare (sau într-un limbaj de programare precum qhasm, care poate genera ansamblul reglat mai fiabil decât un compilator C) și numiți-l din C.

Poate într-o zi, compilatorii C vor adopta a secret calificativ, ca const sau volatil, care forțează compilatorul să genereze numai instrucțiuni de mașină care sunt cunoscute – în anumite modele de CPU! – să ruleze în timp constant atunci când operează pe obiect și împiedică compilatorul să ia comenzi rapide, cum ar fi întreruperi anticipate dependente de secret de la O buclă. Dar acea zi nu este încă aici.

Există, de asemenea, o chestiune despre instrucțiunile de mașină care rulează de fapt în timp constant pe un procesor, care uneori este documentat și uneori de încredere. Deci, pe lângă a face Inginerie pentru a vă construi programele din circuite logice, trebuie să faceți ştiinţă pentru a afla ce operațiuni sunt de fapt sigure de utilizat pe CPU.

Acest lucru ne aduce înapoi la punctul inițial: chiar doriți să vă concentrați efortul de a menține acest lucru într-o rutină de bibliotecă, astfel încât fiecare programator să nu fie nevoit să țină evidența capriciilor compilatoarelor (și a design-urilor CPU!) în codul generat și sincronizare. pe cont propriu și pot lăsa în schimb ursul nostru prietenos de cartier.


Există și alte contramăsuri decât logica în timp constant? Cateodata da.

  • Ai putea injecta zgomot aleatoriu în logica ta, în speranța că va încurca măsurătorile atacatorului. Dar există deja zgomot în măsurătorile lor, cum ar fi programarea în sistemul de operare, așa că trebuie doar să ia mai multe mostre… și se dovedește că zgomotul este nu este o contramăsură foarte eficientă a canalului lateral.

    Mai exact, zgomotul artificial crește costurile atacatorului cu cel mult aproximativ pătratul raportului dintre zgomotul artificial și zgomotul real, ceea ce este mult sub ceea ce este de obicei considerat un decalaj acceptabil pentru securitate în criptografie. Deci, în mare parte, te costă mult timp să nu faci nimic.

  • Ai putea folosi proprietățile algebrice ale criptosistemului pentru a-l randomiza, uneori numite „orbire”. De exemplu, în loc de calcul y^d mod n Unde d este un exponent secret în RSA, ați putea alege r la întâmplare, calculează s := r^e mod n Unde e*d â¡ 1 (mod (n)), înmulțiți y de s a obține (y * r^e) mod n, calculează (y * r^e)^d mod n = (r * y^d) mod n, apoi împărțiți r.

    Multe implementări, cum ar fi OpenSSL, folosesc această abordare, deoarece este o modalitate ușoară de a moderniza o implementare existentă a unui criptosistem precum RSA care are structura algebrică necesară. Nu este o idee rea, așa cum este zgomotul aleatoriu, dar are costuri: trebuie să faceți munca suplimentară pentru randomizare, trebuie să aveți o diviziune modulară sau o logică de inversare, iar canalele laterale ar putea încă scurge informații despre r și d. De exemplu, chiar și exponentiația modulară orbită va scurge greutatea lui Hamming d cu excepția cazului în care luați contramăsuri suplimentare, cum ar fi adăugarea unui multiplu aleatoriu de (n) la d primul... care poate expune canale laterale suplimentare, etc.

  • Pentru cazul specific de comparare a două șiruri de octeți pentru egalitate (de exemplu, două coduri de autentificare a mesajelor), o opțiune rezonabilă este să le distribuiți prin hash cu o familie de funcții pseudoaleatoare precum HMAC-SHA256 sub o cheie secretă unică. k, și verificați dacă HMAC-SHA256_k(x) == HMAC-SHA256_k(y).

    Probabilitatea unui fals pozitiv este 1/2256, care este o probabilitate mai mică decât trebuie să vă faceți griji vreodată. Puteți utiliza în siguranță egalitatea în timp variabil pentru HMAC, deoarece dacă X este nu egal cu y, apoi cantitatea de timp chiar și în naiv Rutina de egalitate a șirurilor de octeți (presupunând că nu se salvează la primul octet zero sau ceva stupid de genul acesta!) va fi independentă de valorile lui X și y: există o probabilitate de 255/256 să se oprească după o iterație, o probabilitate de 65535/65536 după două iterații, etc.

    Desigur, acest lucru vă ajută cu adevărat doar dacă puteți implementa HMAC-SHA256 în timp constant! Din fericire, SHA-256 este proiectat pentru a fi implementat cu ușurință ca circuit logic în timp constant, deci implementările C tind pentru a fi suficient de rezistent la canalele laterale, dar, să zicem, Python vă va pune în probleme din cauza cache-ului întreg mic, dacă nu altceva.


* Terminologia este, din păcate, puțin confuză. Aici timp constant înseamnă că durata de timp nu variază în funcție de intrări, și nu este același cu asimptotic noțiunea de „timp constant” în informatică, adesea scrisă O(1), ceea ce înseamnă doar perioada de timp poate varia în funcție de intrări, dar este delimitată de o constantă. Îmi pare rău. Nu eu am inventat terminologia. S-ar putea să fi ales âfixed-timeâ vs. âtimp variabilâ, dar este prea târziu acumââtimp constantâ este înrădăcinat în literatură.

Puncte:7
drapel id

Iată cei doi cenți ai mei:

Un atac de sincronizare folosește timpul necesar pentru a executa un algoritm bazat pe diferite intrări.

Luați o problemă mai simplă, cum ar fi să găsiți dacă un singur caracter există într-un șir secret. Într-un algoritm tradițional, ați itera peste șir și ați returna un boolean odată ce ați găsit caracterul. Acest lucru ar dura mai mult cu cât un personaj este mai departe, scurgând astfel unele informații din șirul secret.

Dacă am vrea să facem acest timp constant, am face ca găsirea personajului să dureze același timp, indiferent dacă este la început sau la sfârșit, de ex. repetarea întregului șir și revenirea odată ce terminați iterația.

Un alt lucru care necesită mai mult timp sunt ramurile. O ramură este o bucată de cod care poate fi executată sau nu, adică un dacă afirmație.

Exemplu luat dintr-o funcție de multiplicare GF(2^8):

cu ramura:

dacă ((a >> 7) & 1)
    a = (a << 1) ^ polinom;
altfel
    a <<= 1

fără ramuri:

const uint8_t mask = -((a >> 7) & 1); // 0xff dacă este setat cel mai mare bit, altfel 0x00
a = (a << 1) ^ (polinom și mască); // aplică polinomul numai dacă masca este 0xff

După cum puteți vedea, codul cu ramificație poate face încă 1 operație într-unul dintre cazuri, în timp ce codul fără ramuri execută întotdeauna aceleași operații.

Puncte:6
drapel de

Ceea ce ai auzit este corect.

Este o luptă ascendentă. Compilatorul și hardware-ul nu sunt concepute pentru a vă ajuta și sunt concepute în mod deliberat pentru a face lucruri care confundă ceea ce încercați să faceți.

  • Standardul C nu spune nimic despre sincronizare. Compilatorii nu încearcă pentru a vă asigura că, dacă codul dvs. C arată ca un algoritm în timp constant, acesta se compilează întotdeauna în codul mașină în timp constant.

    O abordare este să înveți în detaliu ce poate face compilatorul și cum să eviți optimizările confuze. Marcarea funcţionează ca noinline, de exemplu, ar putea fi important.

    O altă abordare este eliminarea completă a compilatorului din imagine. Puteți să compilați codul C sensibil la sincronizare la asamblare o dată, să examinați ansamblul pentru a vă asigura că este încă un algoritm cu timp constant, să verificați codul de asamblare și să îl utilizați, nu C original, ca sursă atunci când vă construiți programul.

  • La nivel de cod al mașinii există o problemă similară. CPU-urile nu încearcă pentru a se asigura că o secvență constantă de instrucțiuni rulează întotdeauna în timp constant. Multe mecanisme din hardware (cache-urile, în special) încearcă să accelereze lucrurile, dar nu reușesc întotdeauna. Codul poate rula mai lent în cazuri speciale foarte specifice în care accelerarea nu a funcționat. Acest lucru poate scurge informații.

    Nu știu cum să rezolv acest lucru decât cunoscând hardware-ul extrem de bine și măsurând.

user7761803 avatar
drapel vn
„CPU-urile nu încearcă să se asigure că o secvență constantă de instrucțiuni rulează întotdeauna în timp constant” nu, dar unele arhitecturi CPU permit sincronizarea independentă de date a instrucțiunilor individuale, de ex. în Armv8.4A - https://developer.arm.com/documentation/ddi0595/2021-06/AArch64-Registers/DIT--Data-Independent-Timing
Puncte:4
drapel gd

Umilii mei 2 cenți: tehnici de tăiere în bucăți dacă sunt accesibile în cazul dvs.: https://timtaubert.de/blog/2018/08/bitslicing-an-introduction/

EDITAȚI | ×

Nu vreau să dublez (înrăutățindu-le) cuvinte pe care le puteți citi în pagina legată, dar mi s-a cerut ceva mai mult decât un link, așa că să spunem că, în esență, bitslicing simulează o implementare hardware în software: este reprezentat întreg algoritmul ca o secvență de operații booleene atomice, care sunt apoi executate în timp constant. Îmi pare rău că nu pot oferi mai multe detalii, doar recent am aflat despre această tehnică în timpul unui seminar despre cele mai bune practici aplicate de criptografie: nu am aprofundat, dar pare de ce aveți nevoie, având în vedere că doriți să faceți doar rezistent la timp. o parte foarte specifică a codului dvs., astfel încât costul general de implementare este poate accesibil în cazul dvs. (bineînțeles că ar fi un coșmar dureros pentru codul larg)

Puncte:3
drapel kz

Iată vestea bună: algoritmul tău nu trebuie să fie „în timp constant”. Trebuie doar să fie independent de datele de intrare. Este bine dacă există variații aleatorii; ele sunt de fapt utile dacă variațiile aleatoare sunt mai mari decât variațiile dependente de date.

Accesul la memorie rulează în mod notoriu la oră variabilă. Deci, dacă aveți modele de acces dependente de date, este rău. A fost dat un exemplu, tabelul [x[i]].Accesarea x[i] într-o buclă nu este un timp constant, dar timpul nu depinde de date. Accesarea tabelului [x[i]] depinde de date, astfel încât este posibil să obțineți informații despre date.

Operațiile de înmulțire și împărțire pot depinde de date, așa că trebuie să testați acest lucru.

poncho avatar
drapel my
„Trebuie doar să fie independent de datele de intrare”; de fapt, trebuie să fie independent de datele secrete, adică orice lucru la care trebuie să presupunem că adversarul nu are acces. Nu este o problemă dacă, să zicem, timpul necesar operațiunii de semnătură cu cheie publică depinde de mesaj, deoarece presupunem că mesajul este oricum public.
Puncte:2
drapel nc

Dacă puteți accepta „probabil cu timp constant” ca răspuns (mai degrabă decât „cu siguranță cu timp constant”), atunci ați putea evalua experimental dacă codul dumneavoastră este cu timp constant. Faceți așa, vă recomand instrumentul dudect. Pentru a cita GitHub Readme:

Cum funcţionează asta? Pe scurt, acest cod ia două diferite intrări, rulează fragmentul de cod de mai multe ori pentru fiecare intrare și măsură cât timp durează. Dacă există o diferență statistică asupra timpul (mediu) necesar pentru a rula cu diferite intrări, the implementarea nu este considerată constantă de timp. Pentru detalii, citiți hârtie sau aruncați o privire la sursă.

Și:

Codul meu trece aceste teste. Înseamnă că este într-adevăr constantă în timp? Absolut nu. [...]

Un instrument similar este ct-fuzz, care utilizează testarea greybox fuzzy bazată pe acoperire pentru a detecta scurgerile de sincronizare. Mai multe despre metodologia în lucrare ct-fuzz: Fuzzing pentru scurgeri de sincronizare.

Atât cu dudect, cât și cu ct-fuzz, nu puteți fi 100% sigur că codul dvs. C este în timp constant, dar dacă există scurgeri majore de sincronizare, le veți găsi. Și dacă instrumentul nu poate găsi nicio scurgere de sincronizare, atunci puteți fi „în mod rezonabil încrezător” că nu există nicio scurgere.


Alte câteva instrumente pot fi folosite pentru a determina static dacă o bucată de cod C este în timp constant:

  • ctgrind, construit pe valgrind, urmărește fiecare bit de memorie secretă și verifică dacă condiționalele nu depind de biții secreti.

  • CacheAudit, care, potrivit acestuia Citiți-mă GitHub:

CacheAudit este un analizor static al canalelor laterale de cache. CacheAudit ia ca intrare un program binar x86 pe 32 de biți și o configurație cache, și derivă garanții formale, cantitative de securitate pentru a set cuprinzător de adversari cache, și anume cei bazați pe observarea stărilor de cache, a urmelor de lovituri și erori și execuție ori.

Deoarece aceste instrumente folosesc analiza statică, puteți fi mai sigur că nu există scurgeri de sincronizare dacă instrumentul nu a găsit niciuna. Cu toate acestea, niciunul dintre aceste instrumente nu are un model hardware precis despre ce se scurge și ce nu. Aceasta înseamnă că ei presupun că, de exemplu, înmulțirea este în timp constant. Dacă multiplicarea nu este de fapt în timp constant, atunci acele instrumente nu vor detecta potențiale scurgeri de sincronizare. Vezi pe a lui Peter Green Răspuns pentru mai multe despre operațiunile care ar putea scurge, dar sunt probabil presupuse în timp constant de către CacheAudit și ctgrind.

Puncte:1
drapel br

Puteți face măsurători precise de sincronizare folosind funcția intrinsecă __rdtscp(), apoi faceți statistici asupra rezultatelor

Această funcție expune un registru intern de 64 de biți incrementat la fiecare ciclu de ceas, frecvența ceasului fiind frecvența oficială a procesorului așa cum se vede în /proc/cpuinfo după semnul @

Procesoarele de generația a 10-a ar trebui să aibă flag constant_tsc așa cum se vede în /proc/cpuinfo

Postează un răspuns

Majoritatea oamenilor nu înțeleg că a pune multe întrebări deblochează învățarea și îmbunătățește legătura interpersonală. În studiile lui Alison, de exemplu, deși oamenii își puteau aminti cu exactitate câte întrebări au fost puse în conversațiile lor, ei nu au intuit legătura dintre întrebări și apreciere. În patru studii, în care participanții au fost implicați în conversații ei înșiși sau au citit transcrieri ale conversațiilor altora, oamenii au avut tendința să nu realizeze că întrebarea ar influența – sau ar fi influențat – nivelul de prietenie dintre conversatori.