Обзор
База всех адресов — 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 | тело больше лимита |
| 415 | fetch: ответ не 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": "…"}]}
Как проверить аттестацию, ничему не доверяя:
- подпись манифеста сходится с его
operator;
- список статусов подписан
statusKey из манифеста, auditorKey совпадает с operator, а nextUpdate ещё не наступил;
- дайджест файла аттестации совпадает с записью в списке, и запись в состоянии
active;
- аттестация подписана ключом оператора и не истекла; закреплённые документы (область, методика, отчёт) совпадают со своими дайджестами.
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 hub/api/a/{slug}/stellar-link 5 токенов 🔏 ключ аудитора
Связать аудитора с его существующим аккаунтом в основной сети Stellar. Сначала владелец аккаунта одной транзакцией (кабинет аудитора формирует её сам, подписать можно любым кошельком: по QR-коду, ссылкой SEP-7 или в Stellar Lab) пишет на этот аккаунт две записи данных:
onym-audit-auditor | открытый ключ аудитора, 32 байта |
onym-audit-proof | подпись ключа аудитора, 64 байта, над строкой "onym-audit-link-v1\nPublic Global Stellar Network ; September 2015\n" + G-адрес |
Так согласие обеих сторон видно в сети: аккаунт подписал транзакцию, ключ аудитора — доказательство. Аккаунту аудитора не нужно существовать в основной сети. Затем:
{"request": {"action": "stellar-link" | "stellar-unlink", "auditor": "onym:component:<имя>",
"account": "G…", "issuedAt": "…", "signature": "…"}}
Хаб читает аккаунт на horizon.stellar.org и проверяет обе записи. Если всё сходится, он публикует сам подписанный запрос как a/<имя>/stellar-link.json; страница аудитора перепроверяет его в браузере так же.
201 {"account", "uri"}; stellar-unlink → {"unlinked": true} (записи на аккаунте удаляет их владелец).
Ошибки: 403 — подпись не ключа аудитора или несвежий запрос; 422 — записей нет, в них другой ключ или подпись не сходится.
Заказы
Заказать проверку может кто угодно, у любого аудитора с офертами. Браузер создаёт ключ для заказа и подписывает им заказ как проверяемый и как спонсор; аудитор контрподписывает его, когда публикует аттестацию.
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 хранилищ.
Списки
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 + .sig | seat: "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 и сравнивает.