Pull to refresh

Некоторые аспекты разработки платежных систем. Часть II. One time passwords и ECDSA

Reading time6 min
Views3.4K
Доброго здравия, %username%!

В первой части я рассказал как можно минимальными усилиями защитить БД нашей платежной системы. Но, как заметил один из комментирующих, при компрометации web сервера появляется возможность подсмотреть все логины и пароли пользователей. Тут нам на помощь приходят One time passwords (OTP).
Под катом моя вольная интерпритация данного термина с использованием криптографии эллиптических кривых (ECC). Кстати говоря, платежные системы далеко не единственная сфера применения этой технологии.
Upd:
Ахтунг! При взломе веб сервера все таки есть вероятность подмены платежных реквизитов, так что все таки подписывать лучше не случайную строку (хоть это и защитит от полной компрометации системы, но не защитит от случаев, когда подменяются реквизиты прямо во время платежа), а хэш платежного документа, показывая юзеру при этом все реквизиты платежа в программе.
З.Ы. Генерировать ключ лучше тоже на стороне клиента

Чтобы не придумывать одноразовых карточек с паролями решено было применить ЭЦП. Как раз подвернулся случай использовать так любимые мной в силу своей надежности и скорости эллиптические кривые.
Схема была следующей:
  1. При регистрации юзера выдаем ему зашифрованный файлик с открытым\закрытым ключом и паролем от него. Себе оставляем только открытый ключ.
  2. Когда юзер хочет совершить платеж или перевод денег генерируем случайным образом строку, которую ему показываем. Сохраняем её в бд с привязкой к пользователю.
  3. Юзер копирует строку в специальную программу, которая по выданному паролю дешифрует закрытый ключ и подписывает нашу случайную строку.
  4. Юзер отдает нам подпись, мы её проверяем с помощью открытого ключа и даем добро на операцию.


Предстояло писать внешнюю хранимку для MySQL и сопутствующие программы. В качестве криптобиблиотеки был выбран старый добрый OpenSSL. В результате 2х суток без сна на свет появился рабочий вариант из:
  1. Програмки, генерирующей ключевую пару и помещающую её в БД (написал на Builder).
  2. Програмки для пользователя, генерирующей цифровую подпись (на нем же).
  3. Внешней хранимки, эту цифровую подпись проверяющей (пользователю нужен ответ мгновенно, програмки не катят). Написал на VC.


Сразу предупреждаю: код не блещет красотой, C не мой основной ЯП.

Теперь по пунктам:

1) Генерируем ключ
  const KEYSIZE = SHA512_DIGEST_LENGTH+4; // 4 байта на «соль»
  unsigned char *pubkey = (unsigned char *)OPENSSL_malloc(10000),
  *privkey = (unsigned char *)OPENSSL_malloc(10000); //буферы для открытого и закрытого ключей
  AnsiString pub, prv, userid, paypass; //строки для хранения открытого, закрытого ключей и пароля от них
  unsigned char md[KEYSIZE]= {0}; // тут будет хеш от пароля
  unsigned int i = ParamCount();

  paypass = «rAnDom_PaSs»;

  EC_GROUP *group = EC_GROUP_new_by_curve_name(NID_sect571r1); // выбираем эллиптическую кривую
  EC_GROUP_set_point_conversion_form(group,POINT_CONVERSION_UNCOMPRESSED);

  EC_KEY *x = EC_KEY_new();
  EC_KEY_set_group(x,group);
  BIO *out = BIO_new(BIO_s_mem()); // Писать будем в память

  // Код для замедления брутфорса

  env_md_ctx_st mdctx; //Контекст для хэша
  EVP_MD_CTX_init(&mdctx);
  EVP_DigestInit_ex(&mdctx,EVP_sha512(),NULL); //Алгоритм хэширования — SHA512
  EVP_DigestUpdate(&mdctx,paypass.c_str(),strlen(paypass.c_str())); // Хэшируем пароль первый раз

  for (i = 0; i < 0x20000; i++) {

    memcpy(md,mdctx.md_data,SHA512_DIGEST_LENGTH); // копируем хэш в массив

    md[64] = i ^ md[0] ^ md[7] ^ md[5] ^ md[23]; //вычисляем дополнительные 4 байта
    md[65] = i ^ md[1] ^ md[9] ^ md[6] ^ md[53]; //на основе предыдущего хэша
    md[66] = i ^ md[3] ^ md[25] ^ md[11] ^ md[48]; // они будут т.н. раундовой «солью»
    md[67] = i ^ md[8] ^ md[18] ^ md[17] ^ md[2];

    EVP_DigestUpdate(&mdctx,md,KEYSIZE); // и хэшируем предыдущий хэш+соль
  }

  EVP_DigestFinal(&mdctx,md,NULL); //заканчиваем считать хэш
  EVP_MD_CTX_cleanup(&mdctx);

  EC_KEY_generate_key(x); //генерируем ключевую пару

  PEM_write_bio_ECPrivateKey(out, x, EVP_aes_256_cbc(), md, KEYSIZE, NULL, NULL); // пишем ключевую пару, зашифрованную по алгоритму AES-256 с ключом, посчитанным на предыдущем шаге
  BIO_flush(out);
  i = BIO_read(out,privkey,10000); // узнаем количество записанных байт

  // для верности сожмем ZLIBом

  zByte *compr;
  uLong comprLenPrv = 1000*sizeof(int);
  zByte *comprPrv  = (zByte*)calloc((uInt)comprLenPrv, 1);
  compress2(comprPrv, &comprLenPrv, (const Bytef*)privkey, i,Z_BEST_COMPRESSION);

  OPENSSL_free(privkey);
  BIO_free(out);

  out = BIO_new(BIO_s_mem()); //еще один буфер для открытого ключа
  PEM_write_bio_EC_PUBKEY(out,x); //пишем его в память в формате PEM
  BIO_flush(out);
  BIO_read(out,pubkey,10000); // читаем его в кусок памяти

    // Волшебный код, чтобы скопировать закрытый зашифрованный ключ в массив байт
  TByteDynArray cp;
  cp.set_length(comprLenPrv);

  for (i = 0; i < comprLenPrv; i++) {
    cp[i] = comprPrv[i];
  }
    
    //копируем открытый ключ в строку
  AnsiString pubk;
  pubk.sprintf("%s",pubkey);

    
    // освобождаем паметь
  free(comprPrv);
  OPENSSL_free(pubkey);
  BIO_free(out);
  EC_KEY_free(x);  


2) Подписываем на клиенте этим ключом случайную строку, выданную сервером

unsigned long i;
unsigned char *x;
unsigned char buf [1024]={0};

//Подписываем строчку ключом. key — считанный и расшифрованный ключ из п.1. s — рендомная строчка.
ECDSA_SIG *sig = ECDSA_do_sign(s.c_str(),s.Length(),key);

x = buf;
i = i2d_ECDSA_SIG(sig,&x); // конвертим в двоичную форму
x = buf;
ECDSA_SIG_free(sig);
//чтобы подпись можно было скопировать — конвертим её в base64
BIO *b64 = BIO_new(BIO_f_base64());
BIO_set_flags(b64,BIO_FLAGS_BASE64_NO_NL); // без переводов строк
BIO *mem = BIO_new(BIO_s_mem());
mem = BIO_push(b64,mem);
BIO_write(mem,&buf,i);
BIO_flush(mem);

char *res;
BIO_get_mem_data(mem,&res);
mAnswer->Text = res; // выводим пользователю результат.
.

3) Проверяем подпись
int ssl_VerifySignature(const char *key, const char *str, const char *csig)
{
    
    OpenSSL_add_all_algorithms();

    BIO *bkey = BIO_new(BIO_s_mem());
    BIO_write(bkey,key,(int)strlen(key));
    BIO_flush(bkey);

    EC_KEY *ec = PEM_read_bio_EC_PUBKEY(bkey,NULL,NULL,NULL); //читаем открытый ключ
    BIO_free(bkey);

    if (!ec) return -2;

    unsigned long i;
    unsigned char *x;
    unsigned char buf [1024]={0};

    BIO *b64 = BIO_new(BIO_f_base64());
    BIO_set_flags(b64,BIO_FLAGS_BASE64_NO_NL);
    BIO *mem = BIO_new(BIO_s_mem());
    BIO_write(mem,csig,(int)strlen(csig));
    mem = BIO_push(b64,mem);
    x = buf;
    i = BIO_read(mem,x,1024);
    x = buf;

    ECDSA_SIG *sig = ECDSA_SIG_new();
    sig = d2i_ECDSA_SIG(&sig,(const unsigned char **)&x,i); // читаем подпись

    i = ECDSA_do_verify((const unsigned char *)str,(int)strlen(str),sig,ec); // проверяем подпись строки
    BIO_free_all(mem);
    ECDSA_SIG_free(sig);
    return i;
}
.

Таким образом, даже если хакер получит доступ к аккаунту юзера, он не сможет потратить его деньги\сменить пароль\мыло\любые другие функции которые вы посчитаете нужным защитить с использованием этого метода.
А если стырит ключ, то благодаря циклу хэширования пароля с солью 0x20000 (131072) раз запарится брутфорсить даже простые пароли.
Tags:
Hubs:
+13
Comments57

Articles

Change theme settings