Зачем вашему пайплайну мобильного приложения подпись (и как сохранить её в безопасности)
Вы только что собрали Android или iOS приложение. Сборка зелёная, тесты пройдены, вы готовы к релизу. Но прежде чем APK или IPA окажется на устройстве, есть ещё один шаг, который часто воспринимается как бюрократическая формальность: подпись приложения.
Соблазн отнестись к подписи как к галочке велик. Но если вы когда-нибудь теряли keystore, наблюдали истечение сертификата в пятницу вечером или случайно коммитили provisioning profile в публичный репозиторий, вы знаете, что здесь всё становится серьёзно. Подпись — не техническая формальность. Это уровень безопасности, который доказывает, что приложение создано вами, а не кем-то, кто перепаковал его или выдал себя за вас.
Что на самом деле делает подпись
Когда вы подписываете приложение, вы прикрепляете цифровую подпись, которая связывает бинарный файл с вашей личностью. Эта подпись проверяется операционной системой и магазином приложений. Если подпись не совпадает, приложение не установится, или магазин отклонит загрузку.
Для Android подпись использует файл keystore. Этот файл содержит закрытый ключ и цифровой сертификат. Воспринимайте его как официальную печать. Каждый раз, когда вы собираете APK или AAB для релиза, вы ставите эту печать. Если позже вы используете другой keystore, Android будет считать приложение совершенно другим, даже если имя пакета совпадает. Это означает, что пользователи не смогут обновить приложение поверх существующей установки. Им придётся удалить старую версию, потеряв все локальные данные.
Для iOS процесс более сложный. Вам нужно две вещи: сертификат и provisioning profile. Сертификат — это ваша цифровая идентификация как разработчика. Provisioning profile связывает сертификат, App ID и список устройств, которым разрешено запускать приложение. Для распространения через App Store используется дистрибутивный сертификат и App Store provisioning profile. Для внутреннего тестирования или разработки — сертификат разработчика и ad-hoc provisioning profile.
Настоящая проблема: хранение секретов в пайплайне
Как только вы понимаете, что требуется для подписи, возникает очевидный вопрос: как хранить эти учётные данные в вашем CI/CD пайплайне, не записывая их в код или конфигурационные файлы?
Ответ — управление секретами. Но давайте сначала проясним, чего делать не стоит.
Никогда не храните keystore, сертификаты или provisioning profile в вашем Git-репозитории. Эти файлы — секреты, а не конфигурация. Если они окажутся в публичном репозитории, любой сможет подписывать приложения от вашего имени. Если они окажутся в приватном репозитории, проблема всё равно останется: каждый разработчик с доступом к репозиторию теперь владеет вашими ключами подписи для продакшена. Это риск для безопасности и аудита.
Вместо этого используйте хранилище секретов, предоставляемое вашей CI/CD платформой. GitHub Actions, GitLab CI, Jenkins и большинство других платформ имеют встроенные секретные переменные. Вы можете загрузить ваш keystore или сертификат в виде строки в кодировке base64, сохранить как секретную переменную и декодировать обратно в файл во время выполнения пайплайна. Секрет никогда не записывается на диск сборочной машины до момента выполнения и никогда не появляется в логах.
Вот конкретный пример для Android с GitHub Actions:
- name: Decode keystore
run: echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 --decode > app/release.keystore
Для iOS с Fastlane и GitLab CI:
- name: Decode certificate
run: echo $MATCH_PASSWORD | fastlane match import --git_url $MATCH_REPO
Если вашей команде нужен больший контроль, рассмотрите использование выделенного менеджера секретов, такого как AWS Secrets Manager, Azure Key Vault или HashiCorp Vault. Ваш пайплайн получает учётные данные во время выполнения из этих сервисов. Такой подход даёт вам журналы аудита, контроль доступа и централизованную ротацию. Учётные данные никогда не находятся внутри конфигурации самого пайплайна.
Вот полный bash-скрипт, который получает keystore из AWS Secrets Manager, подписывает APK с помощью jarsigner и проверяет подпись:
#!/bin/bash
set -euo pipefail
# Fetch keystore from AWS Secrets Manager
KEYSTORE_SECRET=$(aws secretsmanager get-secret-value \
--secret-id "mobile-app/keystore" \
--query SecretString --output text)
echo "$KEYSTORE_SECRET" | base64 --decode > /tmp/release.keystore
# Sign the APK
jarsigner -verbose -sigalg SHA256withRSA \
-digestalg SHA-256 \
-keystore /tmp/release.keystore \
-storepass "$STORE_PASSWORD" \
app-release-unsigned.apk \
mykeyalias
# Verify the signature
jarsigner -verify -verbose -certs app-release-unsigned.apk
# Clean up
rm -f /tmp/release.keystore
Этот скрипт гарантирует, что keystore никогда не хранится в репозитории, безопасно получается во время выполнения и немедленно удаляется после подписи.
Истечение срока сертификата: тихий убийца пайплайна
Вот сценарий, который случается чаще, чем хотелось бы. Ваш пайплайн работает месяцами без проблем. И вдруг в один день сборка релиза падает. Вы копаетесь в логах и обнаруживаете, что срок действия сертификата подписи истёк. Приложение, уже находящееся в магазине, всё ещё работает на устройствах пользователей. Но вы не можете загрузить новую версию. Магазин отклоняет её, потому что подпись нового бинарного файла недействительна.
У Android keystore и iOS сертификатов есть даты истечения срока действия. Они не обновляются автоматически. Ваш пайплайн должен обнаруживать приближающееся истечение срока и предупреждать команду до того, как учётные данные станут непригодными. Простой скрипт, который проверяет дату действия keystore или сертификата и отправляет уведомление в чат команды, может спасти вас от заблокированного релиза.
Для Android вы можете проверить истечение срока keystore с помощью:
keytool -list -v -keystore release.keystore -storepass $STORE_PASSWORD | grep "Valid until"
Для iOS инструмент Fastlane match включает режим --readonly, который показывает истечение срока сертификата. Вы также можете использовать команду security на macOS:
security find-identity -v -p codesigning | grep "iPhone Distribution"
Практический чек-лист для подписи в вашем пайплайне
Если вы настраиваете подпись впервые или пересматриваете текущую конфигурацию, пройдитесь по этому чек-листу:
- Хранятся ли keystore, сертификаты и provisioning profile вне Git?
- Сохранены ли учётные данные подписи в хранилище секретов CI/CD платформы или внешнем менеджере секретов?
- Закодирован ли keystore или сертификат в base64 и сохранён как секретная переменная, а не в конфигурационном файле?
- Декодирует ли пайплайн секрет только во время выполнения, во временной директории?
- Удаляются ли временные файлы подписи после завершения сборки?
- Проверяет ли пайплайн истечение срока сертификата и предупреждает ли команду до того, как учётные данные станут недействительными?
- Существует ли документированный процесс ротации или обновления учётных данных подписи?
- Ограничен ли доступ к учётным данным подписи для продакшена небольшим кругом доверенных членов команды?
Подпись — это не конец
После того как ваше приложение подписано, артефакт готов к тестированию. Но подписанный бинарный файл, лежащий на сборочной машине, — это не то же самое, что протестированный релиз. Мобильные приложения невозможно полностью проверить, читая код или запуская только модульные тесты. Вам нужно запустить подписанное приложение на эмуляторах, симуляторах или реальных устройствах, чтобы выявить проблемы, которые проявляются только во время выполнения.
Этап подписи — это шлюз. Он гарантирует, что то, что вы собираетесь тестировать и поставлять, действительно ваше. Относитесь к нему с той же осторожностью, что и к учётным данным вашей продакшен-базы данных. Потому что во многих смыслах он ценнее: потеря пароля от базы данных означает восстановление из резервной копии. Потеря keystore означает потерю возможности обновлять ваше приложение полностью.