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.
issuerHTTPS-источник того, кто подписал пропуск, например https://timeclock.kiwi. По нему находятся ключи.
keyIdКаким ключом издателя он подписан.
holderIdОдинаков для человека у одного издателя и отличается у любого другого. Позволяет объекту узнать вернувшегося посетителя. Не должен быть табельным номером, почтой или чем-либо, что идентифицирует человека в другом месте.
issuedAt, expiresAtВремя в ISO 8601. Пропуск должен действовать около минуты и не более 15 минут, чтобы его фото быстро становилось бесполезным.
firstName, lastNameИмя.
companyГде работает, если решил поделиться.
phoneНомер мобильного, желательно в международном формате.
emailАдрес электронной почты.
vehicleRegistrationНомер транспортного средства, на котором приехал.
documentsКарты и сертификаты: у каждого есть type (из списка ниже) и, необязательно, number, issuer и expiresOn (ГГГГ-ММ-ДД).

Обязательно: version, issuer, keyId, holderId, issuedAt, expiresAt, firstName. Всё остальное необязательно и каждый раз выбирается владельцем.

Типы документов

site-safe-passportSite 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-cardCSCS 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. Правила для киосков

  1. Проверьте подпись ключом издателя и срок действия пропуска. Допускайте до 2 минут в обе стороны на расхождение часов.
  2. Получайте ключи только с HTTPS-источника самого издателя и кэшируйте их, чтобы проверять пропуск без интернета.
  3. Покажите данные и домен издателя и дайте посетителю подтвердить до сохранения.
  4. Считайте данные документов введёнными владельцем, а не проверенными. Если объекту нужна проверка, пусть человек посмотрит настоящую карту.
  5. Можно принимать пропуска только от доверенных издателей или от любого издателя с HTTPS, показывая «Verified by <domain>».
  6. Никогда не запрашивайте у приложения владельца больше, чем содержит пропуск. Владелец сам выбирает, чем делиться.

5. Правила для приложений

  1. Создавайте новый пропуск минимум каждую минуту, пока он на экране.
  2. Дайте владельцу выбирать необязательные данные каждый раз или сохранённым выбором, который можно изменить.
  3. Никогда не добавляйте то, что владелец не выбрал, включая работодателя, местоположение и внутренние идентификаторы.

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 их читают, включая пропуска других приложений этого формата.

Мы всегда рады помочь.
Свяжитесь с нами, и мы сделаем всё возможное, чтобы наша система учёта рабочего времени подошла именно вам.