Open Site Pass
Открытый стандарт QR-кодов для регистрации на рабочих объектах. Любое приложение может показать пропуск, а любой киоск может его прочитать.
Версия 1, ноябрь 2026 года. Свободное использование по лицензии CC BY 4.0.
Подрядчики каждую неделю регистрируются на новом объекте, и везде просят одно и то же: имя, компанию, телефон, транспорт, карту Site Safe. Open Site Pass позволяет хранить эти данные в приложении и передавать их, показывая QR-код. Любое приложение может показать пропуск, а любой киоск регистрации может его прочитать, кто бы их ни создал.
Прежде чем написать это, мы искали существующий стандарт. Продукты регистрации посетителей с QR-пропусками используют собственные форматы, работающие только внутри них. Общие стандарты идентификации (W3C Verifiable Credentials, OpenID for Verifiable Presentations, мобильные водительские права ISO 18013-5) требуют приложения-кошелька и многошагового обмена, что избыточно для планшета у ворот объекта. Поэтому это небольшой формат, который любой язык программирования может прочитать и проверить встроенной криптографией.
1. Что в QR-коде
QR-код содержит простой текст из трёх частей, соединённых точками:
OSP1.<pass>.<signature> OSP1означает, что это Open Site Pass версии 1.<pass>это пропуск в JSON, UTF-8, затем base64url без дополнения.<signature>это подпись ECDSA P-256 с SHA-256 над ASCII-текстом<pass>(точно как в QR-коде) в 64-байтовой форме r ‖ s, затем base64url без дополнения. Именно такую форму выдаётcrypto.subtle.signиз Web Crypto.
2. Пропуск
{
"version": 1,
"issuer": "https://timeclock.kiwi",
"keyId": "2026-11-3fa9c1",
"holderId": "b7f3c2e1a94d0c55e1f3b2a8",
"issuedAt": "2026-11-01T09:42:00.000Z",
"expiresAt": "2026-11-01T09:43:00.000Z",
"firstName": "Sam",
"lastName": "Taylor",
"company": "Harbour Electrical Ltd",
"phone": "+64215550142",
"vehicleRegistration": "QKT482",
"documents": [
{ "type": "site-safe-passport", "number": "SS-482913", "expiresOn": "2027-03-31" }
]
} | version | Для этой версии всегда 1. |
|---|---|
| issuer | HTTPS-источник того, кто подписал пропуск, например https://timeclock.kiwi. По нему находятся ключи. |
| keyId | Каким ключом издателя он подписан. |
| holderId | Одинаков для человека у одного издателя и отличается у любого другого. Позволяет объекту узнать вернувшегося посетителя. Не должен быть табельным номером, почтой или чем-либо, что идентифицирует человека в другом месте. |
| issuedAt, expiresAt | Время в ISO 8601. Пропуск должен действовать около минуты и не более 15 минут, чтобы его фото быстро становилось бесполезным. |
| firstName, lastName | Имя. |
| company | Где работает, если решил поделиться. |
| phone | Номер мобильного, желательно в международном формате. |
| Адрес электронной почты. | |
| vehicleRegistration | Номер транспортного средства, на котором приехал. |
| documents | Карты и сертификаты: у каждого есть type (из списка ниже) и, необязательно, number, issuer и expiresOn (ГГГГ-ММ-ДД). |
Обязательно: version, issuer, keyId, holderId, issuedAt, expiresAt, firstName. Всё остальное необязательно и каждый раз выбирается владельцем.
Типы документов
site-safe-passport | Site Safe passport |
|---|---|
public-liability-insurance | Страхование гражданской ответственности |
electrical-registration | Регистрация электрика |
plumbing-gasfitting-registration | Регистрация сантехника, газовщика или дренажника |
forklift-licence | Права на погрузчик (F endorsement) |
first-aid-certificate | Сертификат первой помощи |
working-at-heights | Работа на высоте |
confined-spaces | Замкнутые пространства |
construction-induction-card | Карта вводного инструктажа в строительстве (White Card) |
cscs-card | CSCS card |
Чтобы предложить другой тип, свяжитесь с нами ниже. Киоски сохраняют документы неизвестных типов, но ни с чем их не сопоставляют.
3. Ключи издателя
Издатели публикуют открытые ключи по адресу <issuer>/.well-known/open-site-pass/keys.json:
{
"keys": [
{
"keyId": "2026-11-3fa9c1",
"publicKey": { "kty": "EC", "crv": "P-256", "x": "…", "y": "…" }
}
]
} Каждый publicKey это JWK, который Web Crypto может загрузить напрямую. Продолжайте публиковать ключ минимум день после прекращения подписи им. Ключи Timeclock.Kiwi находятся по адресу https://timeclock.kiwi/.well-known/open-site-pass/keys.json.
4. Правила для киосков
- Проверьте подпись ключом издателя и срок действия пропуска. Допускайте до 2 минут в обе стороны на расхождение часов.
- Получайте ключи только с HTTPS-источника самого издателя и кэшируйте их, чтобы проверять пропуск без интернета.
- Покажите данные и домен издателя и дайте посетителю подтвердить до сохранения.
- Считайте данные документов введёнными владельцем, а не проверенными. Если объекту нужна проверка, пусть человек посмотрит настоящую карту.
- Можно принимать пропуска только от доверенных издателей или от любого издателя с HTTPS, показывая «Verified by <domain>».
- Никогда не запрашивайте у приложения владельца больше, чем содержит пропуск. Владелец сам выбирает, чем делиться.
5. Правила для приложений
- Создавайте новый пропуск минимум каждую минуту, пока он на экране.
- Дайте владельцу выбирать необязательные данные каждый раз или сохранённым выбором, который можно изменить.
- Никогда не добавляйте то, что владелец не выбрал, включая работодателя, местоположение и внутренние идентификаторы.
6. Проверка пропуска в JavaScript
Это всё, что нужно киоску, используя только встроенную криптографию браузера:
async function readPass(text, fetchKeys) {
const [prefix, body, signature] = text.split('.');
if (prefix !== 'OSP1') throw new Error('Not a site pass');
const pass = JSON.parse(atob(body.replace(/-/g, '+').replace(/_/g, '/')));
const keys = await fetchKeys(pass.issuer); // GET <issuer>/.well-known/open-site-pass/keys.json
const jwk = keys.find(k => k.keyId === pass.keyId)?.publicKey;
const key = await crypto.subtle.importKey('jwk', jwk, { name: 'ECDSA', namedCurve: 'P-256' }, false, ['verify']);
const fromB64 = s => Uint8Array.from(atob(s.replace(/-/g, '+').replace(/_/g, '/')), c => c.charCodeAt(0));
const ok = await crypto.subtle.verify({ name: 'ECDSA', hash: 'SHA-256' }, key, fromB64(signature), new TextEncoder().encode(body));
const now = Date.now(), slack = 2 * 60 * 1000;
if (!ok || Date.parse(pass.expiresAt) + slack < now) throw new Error('Not valid');
return pass;
} Использование с Timeclock.Kiwi
Любой, кто вошёл в приложение Timeclock.Kiwi, может показать пропуск из раздела «Мой пропуск на объект», и все киоски регистрации Timeclock.Kiwi их читают, включая пропуска других приложений этого формата.