Генерация и обновление Wallet Pass (pkpass) на сервере мобильного приложения
Файл .pkpass — это ZIP-архив с JSON-описанием, изображениями и подписью. Apple Wallet принимает только пассы, подписанные сертификатом из Apple Developer Program. Это серверная операция: сертификат хранится на бекенде, клиент получает готовый .pkpass файл и открывает его.
Структура pkpass
pass.pkpass/
├── pass.json # описание паса
├── manifest.json # SHA1-хеши всех файлов
├── signature # PKCS#7 подпись manifest.json
├── icon.png # 29x29 pt
├── [email protected] # 58x58 pt
├── [email protected] # 87x87 pt
├── logo.png # до 160x50 pt
└── background.png # опционально, только для boardingPass
pass.json: ключевые поля
{
"formatVersion": 1,
"passTypeIdentifier": "pass.com.yourcompany.membercard",
"serialNumber": "user-12345-2024",
"teamIdentifier": "ABCD1234EF",
"organizationName": "Your Company",
"description": "Карта участника",
"foregroundColor": "rgb(255, 255, 255)",
"backgroundColor": "rgb(15, 82, 186)",
"storeCard": {
"primaryFields": [
{
"key": "member_name",
"label": "Участник",
"value": "Иван Петров"
}
],
"secondaryFields": [
{
"key": "points",
"label": "Баллы",
"value": "1 250",
"changeMessage": "Баллы обновлены: %@"
}
],
"auxiliaryFields": [
{
"key": "tier",
"label": "Статус",
"value": "Gold"
}
],
"backFields": [
{
"key": "terms",
"label": "Условия",
"value": "Баллы действительны 12 месяцев с момента начисления."
}
]
},
"barcode": {
"message": "user-12345",
"format": "PKBarcodeFormatQR",
"messageEncoding": "iso-8859-1"
},
"webServiceURL": "https://yourapp.com/wallet/",
"authenticationToken": "vxwxd7J8AlNNFPS8k0a0FfUFtq0ewzV"
}
Поле webServiceURL + authenticationToken — это механизм push-обновлений. Apple Wallet зарегистрирует устройство на вашем сервере и будет запрашивать обновлённый пасс при изменениях.
Серверная генерация на Python
import hashlib
import json
import zipfile
import io
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.serialization import pkcs12
from cryptography.hazmat.backends import default_backend
from cryptography import x509
from cryptography.hazmat.primitives.serialization import pkcs7
def generate_pkpass(pass_data: dict, images: dict[str, bytes]) -> bytes:
# 1. Сериализуем pass.json
pass_json = json.dumps(pass_data, ensure_ascii=False).encode('utf-8')
# 2. Строим manifest — SHA1 всех файлов
manifest = {}
files = {"pass.json": pass_json, **images}
for filename, content in files.items():
manifest[filename] = hashlib.sha1(content).hexdigest()
manifest_json = json.dumps(manifest).encode('utf-8')
# 3. Подписываем manifest PKCS#7 detached signature
with open("pass-cert.p12", "rb") as f:
p12_data = f.read()
private_key, certificate, chain = pkcs12.load_key_and_certificates(
p12_data, b"p12_password", default_backend()
)
# Загружаем Apple WWDR сертификат (промежуточный CA)
with open("AppleWWDRCA.cer", "rb") as f:
wwdr_cert = x509.load_der_x509_certificate(f.read(), default_backend())
signature = pkcs7.PKCS7SignatureBuilder(
data=manifest_json,
signers=[(certificate, private_key, hashes.SHA256())]
).add_certificate(wwdr_cert).sign(
encoding=serialization.Encoding.DER,
options=[pkcs7.PKCS7Options.DetachedSignature]
)
# 4. Упаковываем в ZIP
buffer = io.BytesIO()
with zipfile.ZipFile(buffer, 'w', zipfile.ZIP_DEFLATED) as zf:
zf.writestr("pass.json", pass_json)
zf.writestr("manifest.json", manifest_json)
zf.writestr("signature", signature)
for filename, content in images.items():
zf.writestr(filename, content)
return buffer.getvalue()
Сертификат Pass Type ID создаётся в Apple Developer Portal → Certificates, Identifiers & Profiles → Identifiers → Pass Type IDs.
Endpoint для скачивания pass
@app.get("/wallet/pass/{user_id}")
async def download_pass(user_id: str, token: str = Query(...)):
user = db.get_user(user_id)
if not verify_token(user, token):
raise HTTPException(403)
pass_data = build_pass_json(user)
images = load_pass_images()
pkpass_bytes = generate_pkpass(pass_data, images)
return Response(
content=pkpass_bytes,
media_type="application/vnd.apple.pkpass",
headers={"Content-Disposition": f"attachment; filename={user_id}.pkpass"}
)
iOS: открытие pkpass
import PassKit
func downloadAndAddPass(url: URL) {
URLSession.shared.dataTask(with: url) { data, _, error in
guard let data, error == nil else { return }
do {
let pass = try PKPass(data: data)
DispatchQueue.main.async {
let vc = PKAddPassesViewController(pass: pass)!
self.present(vc, animated: true)
}
} catch {
print("Invalid pass: \(error)")
}
}.resume()
}
Push-обновления через APNs
При изменении данных (баллы начислены, статус изменился) сервер:
- Получает
pushTokenустройства из базы (Apple Wallet зарегистрировал его при добавлении пасса) - Отправляет пустой push через APNs на
passkitproduction endpoint - Wallet делает GET
/wallet/v1/passes/{passTypeIdentifier}/{serialNumber}с Bearer-токеном
Ответ — обновлённый .pkpass. Wallet применяет изменения и показывает changeMessage если оно задано в pass.json.
Сроки
2–3 дня. Серверная генерация с подписью, endpoint скачивания, iOS-интеграция — 2 дня. Push-обновления через APNs — дополнительно 0,5–1 день. Стоимость рассчитывается индивидуально.







