Циф­ровая под­пись зап­росов к сер­веру — это не какая‑то чер­ная магия или удел избран­ных сум­рачных безопас­ников. Реали­зация этой фун­кци­ональ­нос­ти в мобиль­ном при­ложе­нии впол­не по силам любому хороше­му раз­работ­чику — при усло­вии, что он зна­ет пра­виль­ные инс­тру­мен­ты и под­ход к этой задаче. И если хорошим раз­работ­чиком тебе при­дет­ся ста­новить­ся самос­тоятель­но, то о пра­виль­ных инс­тру­мен­тах и под­ходах я рас­ска­жу в этой статье.

При раз­работ­ке кли­ент‑сер­верных при­ложе­ний под Android есть нес­коль­ко оче­вид­ных спо­собов сде­лать соеди­нение безопас­нее. Кажет­ся, что к 2020 году уже все выучи­ли аббре­виату­ру HTTPS как ман­тру, да и Google со сво­ей сто­роны помога­ет, зап­рещая по умол­чанию HTTP-тра­фик в новых вер­сиях ОС. Чуть более прод­винутые товари­щи зна­ют, что сам по себе HTTPS защища­ет не от всех век­торов атак (при­вет, Мэл­лори!), и нак­ручива­ют SSL Pinning (aka Certificate/Public Key Pinning). Чаще все­го защита канала на этом закан­чива­ется. Да и чес­тно говоря, в боль­шинс­тве слу­чаев этой защиты дос­таточ­но. Осо­бен­но если с помощью шиф­рования поль­зователь­ских дан­ных и про­вер­ки на недове­рен­ное окру­жение лик­видиру­ются дру­гие век­торы ата­ки.

Но быва­ет и по‑дру­гому. При­ложе­ние вынуж­дено работать в недове­рен­ной сре­де, а это зна­чит, что злов­ред на кли­ент­ском устрой­стве может перех­ватить токены дос­тупа к сер­веру пря­мо из памяти при­ложе­ния. Далее, в зависи­мос­ти от реали­зации механиз­ма инва­лида­ции этих токенов, зло­умыш­ленник какое‑то вре­мя может выпол­нять зап­росы от лица поль­зовате­ля. У этой проб­лемы есть решение — вешать циф­ровую под­пись на все зап­росы, выпол­няющиеся из авто­ризо­ван­ной зоны. Как пра­вило, это все зап­росы, которые не /login или /register. О том, как реали­зовать под­пись зап­росов на кли­енте и на сер­вере, а так­же о под­водных кам­нях и огра­ниче­ниях этой тех­ники погово­рим в статье.

 

Криптоликбез

Что­бы сде­лать повес­тво­вание более сис­темным, давай для начала син­хро­низи­руем­ся в поняти­ях и осве­жим зна­ния крип­тогра­фии, если они по какой‑то при­чине зап­лесне­вели.

Нач­нем с понятия циф­ровая под­пись. Тема ЦП доволь­но обширная, поэто­му огра­ничим­ся асим­метрич­ной схе­мой циф­ровой под­писи, в которой учас­тву­ют откры­тый и зак­рытый клю­чи. В самом прос­том слу­чае циф­ровая под­пись работа­ет по сле­дующе­му алго­рит­му:

  1. Али­са шиф­рует документ сво­им зак­рытым клю­чом, тем самым под­писывая его.
  2. Али­са отправ­ляет под­писан­ный документ Бобу.
  3. Боб рас­шифро­выва­ет документ с помощью откры­того клю­ча Али­сы, тем самым про­веряя под­пись.

Это работа­ет, но есть проб­лема. Если документ, под­писан­ный Али­сой, — чек на некото­рую сум­му денег, то неб­лагона­деж­ный Боб смо­жет обна­личи­вать этот чек, пока у Али­сы не закон­чатся день­ги на сче­те или пока Боба не пой­мают. Для борь­бы с этой проб­лемой при­меня­ются мет­ки вре­мени. Али­са добав­ляет к докумен­ту текущее вре­мя и шиф­рует его вмес­те с докумен­том. Банк, в который Боб при­носит этот чек и откры­тый ключ Али­сы, рас­шифро­выва­ет документ и сох­раня­ет мет­ку вре­мени. Теперь при попыт­ке обна­личить такой чек пов­торно банк заб­локиру­ет эту опе­рацию, так как мет­ки вре­мени будут оди­нако­вые.

Еще не зас­кучал? Потер­пи, это все нам при­годит­ся уже ско­ро, ког­да будем писать реали­зацию. Финаль­ный аспект, который хочет­ся обсу­дить, — про­изво­дитель­ность асим­метрич­ных крип­тосис­тем. Они ока­зыва­ются доволь­но неэф­фектив­ны на боль­ших мас­сивах дан­ных, а зна­чит, попыт­ка при­менить этот под­ход для под­писи объ­емных зап­росов будет нещад­но жрать батарею смар­тфо­на и замед­лять обще­ние с сер­вером. Для уско­рения всей этой машине­рии при­нято исполь­зовать односто­рон­ние хеш‑фун­кции. Ито­говая вер­сия алго­рит­ма будет выг­лядеть так:

  1. Али­са вычис­ляет зна­чение хеш‑фун­кции для докумен­та.
  2. Али­са шиф­рует это зна­чение сво­им зак­рытым клю­чом, тем самым под­писывая документ.
  3. Али­са посыла­ет Бобу документ и под­писан­ное хеш‑зна­чение.
  4. Боб вычис­ляет зна­чение хеш‑фун­кции для докумен­та, прис­ланно­го Али­сой.
  5. Боб рас­шифро­выва­ет зна­чение хеш‑фун­кции докумен­та, прис­ланно­го Али­сой.
  6. Боб срав­нива­ет это зна­чение с вычис­ленным самос­тоятель­но. Если они сов­пада­ют, то под­пись под­линна.
Как работает цифровая подпись
Как работа­ет циф­ровая под­пись

Как вид­но из при­меров — надеж­ность механиз­ма циф­ровой под­писи базиру­ется на двух пред­положе­ниях:

  1. Зак­рытый ключ Али­сы дос­тупен толь­ко ей и боль­ше никому.
  2. У Боба находит­ся откры­тый ключ имен­но Али­сы, а не кого‑то дру­гого.
 

Реализация клиентской части

Те­перь ты дол­жен при­мер­но пред­став­лять, как мож­но реали­зовать под­пись зап­росов. Спо­собов реали­зации боль­ше одно­го, но я покажу самый, по моему мне­нию, прос­той и удоб­ный.

Для начала опре­делим­ся с генера­цией клю­чей и с самим алго­рит­мом циф­ровой под­писи. Очень не рекомен­дую писать это все руками, исполь­зуя крип­топри­мити­вы из Android SDK. Луч­ше взять готовое и зареко­мен­довав­шее себя решение — биб­лиоте­ку Tink, написан­ную сум­рачны­ми гени­ями из Google. Она реша­ет сра­зу нес­коль­ко наших проб­лем:

  • сох­раня­ет клю­чи в Android Keystore, что прак­тичес­ки исклю­чает их насиль­ствен­ное извле­чение с устрой­ства. А зна­чит, обес­печива­ет нам истинность пер­вого пред­положе­ния о надеж­ности механиз­ма циф­ровой под­писи;
  • пре­дос­тавля­ет надеж­ный алго­ритм под­писи на эллипти­чес­ких кри­вых — ECDSA P-256;
  • пре­дос­тавля­ет удоб­ные крип­топри­мити­вы и API для соз­дания циф­ровой под­писи.

Под­клю­чаем биб­лиоте­ку к про­екту (implementation 'com.google.crypto.tink:tink-android:1.5.0') и генери­руем пару клю­чей, которые сра­зу будут сох­ранены в Android Keystore:

companion object {
const val KEYSET_NAME = "master_signature_keyset"
const val PREFERENCE_FILE = "master_signature_key_preference"
const val MASTER_KEY_URI = "android-keystore://master_signature_key"
}
SignatureConfig.register()
val privateKeysetHandle = AndroidKeysetManager.Builder()
.withSharedPref(application, KEYSET_NAME, PREFERENCE_FILE)
.withKeyTemplate(EcdsaSignKeyManager.ecdsaP256Template())
.withMasterKeyUri(MASTER_KEY_URI)
.build()
.keysetHandle

Что­бы сер­вер мог про­верить нашу циф­ровую под­пись, ему нуж­но как‑то передать пуб­личный ключ от той пары, которую мы сге­нери­рова­ли выше. Делать это пра­виль­нее все­го на эта­пе авто­риза­ции. Пуб­личный ключ не сек­ретный, поэто­му мы впол­не можем передать его пря­мо в зап­росе вмес­те с логином и паролем поль­зовате­ля, пред­варитель­но закоди­ровав в Base64:

val bos = ByteArrayOutputStream()
val w = BinaryKeysetWriter.withOutputStream(bos)
privateKeysetHandle.publicKeysetHandle.writeNoSecret(w)
val response = api.login(
LoginRequest(
username,
password,
Base64.encodeToString(bos.toByteArray(), Base64.DEFAULT)
)
)
bos.close()

Tink не поз­воля­ет работать с клю­чевым матери­алом нап­рямую. Вмес­то это­го биб­лиоте­ка пред­лага­ет кон­цепцию Reader/Writer’ов, которые поз­воля­ют читать и писать клю­чи в JSON-пред­став­лении или в бинар­ном. Под­робнос­ти есть в докумен­тации.

Продолжение доступно только участникам

Материалы из последних выпусков становятся доступны по отдельности только через два месяца после публикации. Чтобы продолжить чтение, необходимо стать участником сообщества «Xakep.ru».

Присоединяйся к сообществу «Xakep.ru»!

Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее

Оставить мнение