onym audit · аудитор сети Onym

Для разработчиков

HTTP API

Всё, что можно прочитать, проверить и отправить: адреса, тела запросов, проверки на стороне сервера, ответы и лимиты. Каждое изменение — подписанный документ; сервер хранит байты, но подделать их не может.

Обзор

База всех адресов — https://foldy.io/audit/; пути ниже указаны относительно неё. Сервер состоит из трёх частей:

  • основное место «onym audit» — дерево подписанных документов в корне и два адреса для записи (orders, responses);
  • хаб — любой может стать аудитором (hub/api/…), его документы лежат в a/<имя>/;
  • Discovery-провайдер — подписанные каталоги в discovery/, которые читают приложения Onym.

Учётных записей, паролей и cookies нет. Всё, что меняет состояние, подписано ключом Ed25519 того, кто действует: аудитора, заказчика или проверяемого. Сервер проверяет подпись и хранит байты ровно такими, какими их подписали; подделать документ он не может, потому что не держит ни одного ключа аудитора. Всё, что сервер отдаёт, можно проверить без доверия к нему — в браузере это делает verify.js.

Форматы самих документов (манифест, аттестация, список статусов, заказ, оферта, ответ, отзыв) задаёт профиль реализации. Эта страница описывает транспорт: адреса, тела запросов, проверки, ответы и лимиты. Исходный код — github.com/Dubieroj/onym-audit (hub/, server/).

Общие правила

Канонический JSON

Тот же, что в Discovery-Static-Ed25519 §3:

  • верхний уровень — объект; ключи отсортированы по байтам UTF-8 на любой глубине; запись компактная, без пробелов;
  • экранируются только ", \ и U+0000–U+001F (двухсимвольные формы, где они есть, иначе \u00xx в нижнем регистре);
  • числа — только неотрицательные целые до 253−1;
  • дубликаты ключей, невалидный UTF-8 и одиночные суррогаты отклоняются, а не исправляются.

Сервер канонизирует всё, что принимает, и хранит канонические байты.

Подписи, ключи, дайджесты, время

ПодписьEd25519 над каноническими байтами документа без поля signature (удаляется только на верхнем уровне); значение — стандартный base64 с паддингом. Рядом с каждым подписанным файлом лежит .sig: та же строка и перевод строки.
Ключonym:key:<64 hex в нижнем регистре> — открытый ключ Ed25519. Отпечаток на страницах — первые 8 байт SHA-256 ключа через двоеточие.
Дайджестsha256:<64 hex> от точных байтов файла.
ВремяRFC 3339 в UTC с точностью до секунды: 2026-09-25T10:00:00Z. Другие формы отклоняются.

Подписанный запрос

Адреса с пометкой 🔏 подпись принимают {"request": {…}}. Внутренний объект подписан названным ключом, содержит issuedAt, который отличается от часов сервера не больше чем на ±10 минут, и не содержит лишних полей. Так браузер доказывает владение ключом без сессий и паролей:

import { canonical, parseStrict } from "https://foldy.io/audit/verify.js";

const now = new Date().toISOString().replace(/\.\d+Z$/, "Z");
const req = parseStrict(JSON.stringify({ action: "order-status", orderId, sponsor: key, issuedAt: now }));
const sig = await crypto.subtle.sign("Ed25519", privateKey, new TextEncoder().encode(canonical(req)));
req.set("signature", btoa(String.fromCharCode(...new Uint8Array(sig))));

await fetch("https://foldy.io/audit/hub/api/a/alice/order-status", {
  method: "POST", headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ request: JSON.parse(canonical(req)) }),
});

Ответы и ошибки

Ответы — JSON с Cache-Control: no-store. Ошибка хаба — {"error": "<текст>"}; ошибка основного места — {"error": "<код>", "detail": "<текст>"}.

400тело не разбирается или содержит неизвестное поле
403подпись не того ключа или несвежий issuedAt
404нет такого аудитора, аттестации или открытой заявки
409конфликт с уже опубликованным: неизменяемый документ, откат версии, занятое имя
413тело больше лимита
415fetch: ответ не JSON и не текст
422документ не прошёл проверку — причина в тексте ошибки
429превышен лимит запросов
502внешний адрес недоступен или отвечает не так
503очередь заполнена или хаб занят тяжёлыми проверками
507хранилище аудитора на хабе заполнено (20 МиБ)

Лимиты

  • Тело запроса к хабу — до 4 МиБ; каждый документ — до 1 МиБ; дерево одного аудитора — до 20 МиБ.
  • Скорость — «ведро токенов» на IP-адрес (для IPv6 — на всю /64): ёмкость 60, пополнение 1 токен в секунду. Цена вызова указана у каждого адреса. GET-запросы списков не ограничены.
  • «Тяжёлые» вызовы (digest, conformance) — не больше двух одновременно на весь хаб.
  • Основное место: заказ — до 64 КиБ, ответ — до 16 КиБ, общий лимит 20 запросов с пополнением 1 в секунду.

Статические файлы

Документы отдаются как есть, побайтно, с Access-Control-Allow-Origin: *, X-Content-Type-Options: nosniff и Cache-Control: no-cache; без cookies и журналов доступа. Страницы работают с CSP script-src 'self'.

Чтение и проверка

У основного места (корень /audit/) и у каждого аудитора хаба (a/<имя>/) одинаковое дерево:

manifest.json + .sigподписанный манифест аудитора: ключ оператора, делегированный ключ статусов, политики, методики, оферты
status.json + .sigсписок статусов, подписанный делегированным ключом; переподписывается каждые 6 часов, nextUpdate — через 48 часов
attestations/<id>.jsonаттестации — неизменяемые
revocations/<id>.jsonподписанные отзывы
responses/<аттестация>/<id>.jsonподписанные ответы проверяемых
offers/<id>.jsonоферты: методика, цена, срок, условия раскрытия
orders/<id>.jsonзаказы с тремя подписями, публикуются вместе с аттестацией
scopes/, reports/, methodology/, policies/документы, закреплённые дайджестами

Запись списка статусов:

{"attestationId": "onym-discovery-2026-09-24",
 "attestation": {"uri": "https://foldy.io/audit/attestations/onym-discovery-2026-09-24.json",
                 "digest": "sha256:…"},
 "state": "active" | "revoked" | "superseded" | "expired",
 "supersededBy": null, "revocation": null,
 "responses": [{"uri": "…", "digest": "…"}]}

Как проверить аттестацию, ничему не доверяя:

  1. подпись манифеста сходится с его operator;
  2. список статусов подписан statusKey из манифеста, auditorKey совпадает с operator, а nextUpdate ещё не наступил;
  3. дайджест файла аттестации совпадает с записью в списке, и запись в состоянии active;
  4. аттестация подписана ключом оператора и не истекла; закреплённые документы (область, методика, отчёт) совпадают со своими дайджестами.
import { verifyAttestation } from "https://foldy.io/audit/verify.js";
const get = (p) => fetch("https://foldy.io/audit/" + p).then((r) => r.text());
const d = await verifyAttestation({
  manifestText: await get("manifest.json"),
  statusText: await get("status.json"),
  attText: await get("attestations/onym-discovery-2026-09-24.json"),
  target: null, credited: true,
});
// d.display: "attested" | "revoked" | "superseded" | "expired" | "status-unknown" | …

Страница каждой аттестации — verdict/?id=<id> для основного места и verdict/?a=<имя>&id=<id> для аудитора хаба — делает эти проверки в браузере и показывает каждую.

Основное место «onym audit»

POST orders

Принять заказ (Audit.md §6). Тело — либо голый заказ, подписанный проверяемым и спонсором, либо конверт:

{"order": {…}, "scopeText": "что проверить", "contact": "любой текст, необязательно"}

В конверте order.scope.uri = <база>scopes/order-<orderId>.md, а SHA-256 от scopeText (1–8192 байт) равен order.scope.digest. Контакт — до 256 байт без управляющих символов; он не публикуется.

202 {"orderId", "state": "queued-for-review", "digest"} — заказ в очереди; это ещё не согласие аудитора.

Ошибки: 422 order_invalid, 409 order_conflict (под этим id уже другой заказ), 503 inbox_full (1000 заказов), 413 oversize, 429 rate_limited.

POST responses

Ответ проверяемого на аттестацию (Audit.md §5.7.5). Тело — голый подписанный ответ, до 16 КиБ; attestationId и responseId — 16–128 символов [A-Za-z0-9_-]. После записи список статусов переподписывается, и ответ появляется рядом с аттестацией.

201 {"uri", "digest"}

Ошибки: 404 attestation_unknown, 422 subject_response_invalid, 409 response_conflict — ответы неизменяемы.

Хаб: регистрация и публикация

Аудитор хаба держит свой ключ в браузере (кабинет аудитора) — хаб видит только подписанные документы. Путь аудитора: claim → подписать манифест → register → publish аттестаций → при необходимости revoke.

POST hub/api/claim 5 токенов

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

{"slug": "my-auditor"}

Имя: ^[a-z][a-z0-9-]{2,31}$, не из зарезервированных (admin, api, hub, audit, onym, onym-audit, foundation, sobor, root, status, www). Бронь живёт 30 минут; повторный вызов в этот срок возвращает ту же бронь.

200 {"slug", "statusKey", "base", "componentId", "expires"}

Ошибки: 409 — имя занято; 422 — имя недопустимо.

POST hub/api/register 10 токенов

Опубликовать или заменить манифест вместе с документами, на которые он ссылается.

{"slug": "my-auditor",
 "manifest": {…подписанный манифест…},
 "docs": {"policies/independence.md": "текст", "offers/manual-review.json": "{…}", …}}
  • componentId = onym:component:<имя>; statusEndpoint = <база>status.json; statusKey — выданный при claim;
  • contact — любой текст до 256 байт без управляющих символов (ссылкой на страницах он становится, только если начинается с mailto: или https://);
  • повторная регистрация — только тем же ключом оператора (иначе 403) и не старее текущей: validUntil не раньше, а при равном — те же байты (иначе 409);
  • пути документов: (policies|methodology|scopes|reports|offers|notes)/<имя>.(md|json); принимаются только документы, на которые ссылается манифест;
  • каждая ссылка манифеста указывает либо на файл этого аудитора, либо на общий документ основного места, и дайджест совпадает;
  • оферты подписаны ключом аудитора, относятся к методике из манифеста, не истекли и не откатывают опубликованную версию.

201 {"page", "manifest", "digest"}

POST hub/api/a/{slug}/publish 5 токенов

Опубликовать аттестацию с её документами.

{"attestation": {…},
 "docs": {"scopes/….md": "…", "reports/….json": "…", "orders/<orderId>.json": "…"}}
  • подписана ключом аудитора, status = statusEndpoint, unsolicited.policy = дайджест опубликованной политики;
  • id новый: аттестации неизменяемы, исправление — новая аттестация, которая заменяет старую (supersede);
  • методика, область, шкала и отчёт закреплены так же, как при регистрации;
  • для engagement: "commissioned" прикладывается ровно один заказ с тремя подписями (проверяемый, спонсор, аудитор); orderRef — его дайджест; проверяемый, спонсор, артефакт, методика и область совпадают.

201 {"uri", "digest"} — опубликовано.
202 {"uri", "digest", "held": true, "releaseAt"} — провал по заказу с эмбарго: аттестация, отчёт, заказ и область удерживаются хабом и выходят сами, когда эмбарго кончится.

POST hub/api/a/{slug}/revoke 5 токенов

{"revocation": {…подписанный отзыв…}}

Отзыв принимается всегда, даже когда хранилище заполнено.

201 {"uri"}

Ошибки: 404 — нет аттестации; 409 — уже отозвана.

POST a/{slug}/responses 5 токенов

Ответ проверяемого на аттестацию аудитора хаба. Тело — голый подписанный ответ. Не больше 10 ответов на аттестацию; ответы неизменяемы (409).

201 {"uri"}

GET hub/api/a/{slug}/export

Список всех файлов дерева аудитора — чтобы перенести его на свой сервер: хаб — заменяемый вариант по умолчанию.

200 {"base", "files": ["manifest.json", …]}

Заказы

Заказать проверку может кто угодно, у любого аудитора с офертами. Браузер создаёт ключ для заказа и подписывает им заказ как проверяемый и как спонсор; аудитор контрподписывает его, когда публикует аттестацию.

POST a/{slug}/orders 10 токенов

{"order": {…}, "scopeText": "что проверить", "contact": "необязательно"}
  • заказ подписан проверяемым и спонсором и точно соответствует оферте аудитора: методика, модель цены, условия раскрытия;
  • fee.offerId — оферта из манифеста или rsp-…, ответ на ещё открытую заявку (тогда спонсор — автор заявки, а проверяемый, методика и артефакт совпадают с заявкой);
  • scope.uri = <база>scopes/order-<orderId>.md, scopeText (1–8000 байт) хешируется в scope.digest;
  • контакт необязателен: любой текст до 256 байт; его видит только аудитор.

202 {"orderId", "state": "queued-for-review", "digest"}. Заявка, на которую ответили офертой, закрывается.

Ошибки: 409 — заказ уже выполнен или под этим id другой; 503 — в очереди 100 заказов.

POST hub/api/a/{slug}/order-status 1 токен 🔏 ключ спонсора

{"request": {"action": "order-status", "orderId": "…", "sponsor": "onym:key:…",
             "issuedAt": "…", "signature": "…"}}

200 {"queued": bool, "held": bool} — ждёт ли заказ в очереди и не удерживается ли его результат под эмбарго. Никто, кроме владельца ключа заказа, этого не узнает.

POST hub/api/a/{slug}/inbox 2 токена 🔏 ключ аудитора

{"request": {"action": "list" | "decline", "auditor": "onym:component:<имя>",
             "orderId": "…только для decline", "issuedAt": "…", "signature": "…"}}

list → {"orders": [{"order", "scopeText", "contact", "receivedAt"}], "held": [{"attestationId", "releaseAt"}]}
decline → {"declined": "<orderId>"}

Заявки

Если заказчик не знает, к кому обратиться, он публикует заявку: точные байты, проверяемый компонент, методику и область — всем аудиторам или одному Stellar-адресу. Аудиторы отвечают подписанными офертами, заказчик выбирает одну и заказывает по ней.

POST hub/api/requests 5 токенов

{"request": {
  "requestVersion": 1,
  "requestId": "req-<20 символов [a-z0-9]>",
  "to": null | "onym:key:…",
  "subject": "onym:component:<имя>",
  "methodologyClass": "security-review" | "conformance-run",
  "artifact": {"kind": "source" | "build" | "deployment", "source": "…", "revision": "…", "artifactHash": null},
  "scopeText": "1–8000 байт",
  "requester": "onym:key:…",
  "issuedAt": "…", "expiresAt": "… (не позже чем через 30 дней)",
  "signature": "…ключом requester"
}}

to: null — заявка видна всем; иначе только адресату. conformance-run принимается только для kind: "deployment".

201 {"requestId", "digest"}

Ошибки: 409 — такой id уже есть; 503 — открыто 500 заявок.

GET hub/api/requests

200 — открытые публичные заявки, от новых к старым: [{…заявка, "responses": N}].

POST hub/api/requests/for 2 токена 🔏 ключ адресата

{"request": {"action": "requests-for", "key": "onym:key:…", "issuedAt": "…", "signature": "…"}}

200 — заявки, адресованные этому ключу.

POST hub/api/requests/{id}/responses 5 токенов

Аудитор отвечает офертой.

{"slug": "my-auditor", "offer": {…}}

offerId = rsp-<суффикс requestId>; оферта подписана ключом аудитора, методика — из заявки; на адресную заявку отвечает только адресат; область закреплена; оферта не истекла и не откатывает прежнюю.

201 {"offer": "<uri>"}

Ошибки: 404 — заявка закрыта или истекла.

POST hub/api/requests/{id}/mine 2 токена 🔏 ключ автора заявки

{"request": {"action": "responses", "requestId": "…", "issuedAt": "…", "signature": "…"}}

200 {"open": bool, "responses": [{"slug", "name", "operator", "fingerprint", "page", "completedOrders", "customers", "offer"}]}

Хранилище

POST hub/api/vault 2 токена 🔏 ключ хранилища

Чтобы один и тот же заказчик видел свои заказы и заявки на любом устройстве, приложение хранит их список зашифрованным (AES-256-GCM, ключ выводится из фразы) под отдельным ключом подписи, выведенным из той же фразы. Хаб хранит шифртекст, который не может прочитать, под ключом, который больше нигде не встречается.

{"request": {"action": "get" | "put", "key": "onym:key:…", "data": "base64, только для put",
             "issuedAt": "…", "signature": "…"}}

get → {"data", "updatedAt"} или {"data": null}
put → {"updatedAt"}; данные — до 256 КиБ; всего на хабе — до 20 000 хранилищ.

Инструменты проверки

Браузер не может сам прочитать большинство внешних адресов (CORS), поэтому хаб делает это за него — только публичный HTTPS, без cookies, никогда не во внутреннюю сеть, не больше трёх перенаправлений.

POST hub/api/fetch 2 токена

{"url": "https://…"}

Только JSON и текст, до 1 МиБ (иначе 415).

200 {"url", "status", "contentType", "setsCookie", "digest", "size", "body"}

POST hub/api/digest 20 токенов

{"url": "https://…/app.apk"}

Хеширует файл потоком, до 300 МиБ, не возвращая его, — чтобы закрепить точные байты сборки.

200 {"url", "status", "digest", "size"}

POST hub/api/conformance/discovery 20 токенов

{"manifestUrl": "https://…/manifest.json"}

Запускает набор проверок соответствия Discovery-провайдера (42 проверки) против манифеста с "seat": "discovery"; к другим сервисам набор не применяется (422). Таймаут — 3 минуты.

200 — канонический отчёт: {suite, suiteVersion, runAt, manifestUri, result, documents: [{role, uri, digest, size}], checks: [{id, clause, level, title, outcome, detail}]}

Списки

GET hub/api/auditors

[{"slug", "name", "operator", "fingerprint", "page", "attestations", "offers", "completedOrders", "customers"}]

GET hub/api/seat

То же для основного места ("slug": "").

GET hub/api/library

Все аттестации всех аудиторов, от новых к старым. Это указатель, а не авторитет: каждую запись браузер проверяет по подписанным файлам.

[{"attestationId", "auditor", "auditorSlug", "auditorBase", "fingerprint", "subject", "result", "state", "methodologyClass", "kind", "source", "engagement", "issuedAt", "expiresAt", "orderRef", "uri"}]

curl -s https://foldy.io/audit/hub/api/library | jq '.[0]'

Discovery-провайдер

Провайдер onym:component:onym-audit-discovery публикует подписанные каталоги, которые приложения Onym подключают в Settings → Discovery Providers. Всё — статические файлы:

discovery/manifest.json + .sigseat: "discovery", профиль static-ed25519-v1, возможности signed-snapshot-v1 и local-filtering-v1
discovery/catalogs/onym-auditors.jsonаудиторы (seatTypes: ["audit"]), до 512 записей
discovery/catalogs/onym-audited-services.jsonсервисы Onym, по которым есть действующий аудит, со статусом
discovery/policies/*.md, discovery/privacy.mdполитики, закреплённые дайджестами в манифесте
discovery/status/<компонент>/страница статуса, на которую ссылается запись каталога

Снимки переподписываются при каждом изменении на хабе, но не чаще раза в 2 минуты, и отдаются с Cache-Control: no-cache. Поля снимка: {version, catalogId, providerId, implementationProfileId, policyDigest, sequence, previousDigest, generatedAt, expiresAt, entries, signature}. Запись в onym-audited-services:

{"componentId": "onym:component:onym-relayer", "seatType": "notary",
 "operator": "onym:key:…", "manifest": {"uri": "…", "digest": "sha256:…"},
 "profiles": […], "relationship": "other-disclosed", "placement": "policy-ranked",
 "listedAt": "…", "evidence": [],
 "status": {"state": "review" | "warning", "uri": "https://foldy.io/audit/discovery/status/onym-relayer/"}}

Приложение показывает status плашкой у сервиса и ссылкой в окне согласия. Отпечаток ключа провайдера в приложении — первые 16 hex-символов ключа оператора: 4074 4514 be2a 1414.

Якорь в Stellar

Реестр аудитора закрепляется в тестовой сети Stellar: запись данных onym-audit-status на аккаунте аудитора (это тот же ключ оператора) хранит SHA-256 от канонического JSON

{"anchorVersion": 1, "auditor": "onym:component:…",
 "entries": [{"attestationId", "attestation": "sha256:…", "state", "supersededBy", "revocation"}, …]}

с записями, отсортированными по attestationId. Переподписание ради свежести якорь не меняет; выпуск и отзыв — меняют. Сеть хранит каждую запись со временем леджера, поэтому реестр нельзя задним числом переписать или показать разным читателям по-разному. Проверка — stellar.js: anchorState(account, statusText) спрашивает публичный узел Horizon и сравнивает.