CleanSign API
REST-API сервиса CleanSign для проверки электронных подписей (ЭП/ЭЦП). Принимает документы и файлы подписи через multipart/form-data, возвращает JSON со сведениями о подписантах, цепочкой сертификатов и классификацией подписи. Использует тот же движок проверки, что и веб-форма на главной странице. Подходит для пакетной обработки документов и мониторинга срока действия сертификатов; готовых коннекторов к учётным системам и операторам ЭДО сервис не поставляет — встраивание выполняется на стороне клиента.
https://api.cleanvoice.ru/cleansign/api/v1.
Какие проверки выполняются
- Соответствие хеша документа подписанному атрибуту
messageDigest— отсекает подмену подписанного содержимого. - Криптографическая проверка подписи (ГОСТ Р 34.10-2012, RSA, ECDSA).
- Построение цепочки сертификатов до закреплённого trust-anchor (ГУЦ Минцифры) в режиме fail-closed.
- Проверка привязки
signingCertificateV2(RFC 5126), исключающая подмену сертификата подписанта. - Проверка аккредитации УЦ-эмитента в реестре Минцифры на текущий момент и на момент подписания.
- Опционально: проверки OCSP и CRL, верификация контр-подписей и меток времени CAdES (профили BES — A).
- Классификация подписи по 63-ФЗ — КЭП (она же УКЭП — усиленная квалифицированная), НЭП (усиленная неквалифицированная) или иная — с указанием класса средства ЭП (КС1, КС2, КС3) и СКЗИ.
Поддерживаемые форматы
- CMS / PKCS#7 — отделённая подпись в виде самостоятельного файла рядом с документом. Расширения:
.sig,.sgn,.sign,.p7s,.bin(плюс числовые суффиксы вида.sig2,.sig.001). Подпись принимается в форматах raw DER, base64 и PEM — обёртка определяется автоматически. Кроме того, если файл имеет нестандартное расширение (например, оператор переименовал.sigв.txtили вставил base64 подписи в текстовый файл) и его размер не превышает 256 KB, сервис проверяет содержимое и распознаёт CMS по сигнатуреsignedData— после этого файл встаёт в пайплайн как обычная подпись. Документом может быть любой файл —.pdf,.docx,.xlsx,.doc,.xml,.txt,.html,.rtfи любой бинарный. - CAdES — те же расширения, что и CMS; распознаются профили BES, EPES, T, C, X-L Type 1, A. Штампы времени RFC 3161 проверяются end-to-end (TSA-подпись + сравнение imprint), для CAdES-A реконструируется imprint archive-timestamp v2.
- PAdES — встроенная подпись в PDF (
.pdf): чтение/ByteRange,/Contents, поддержка нескольких подписей в одном документе и словаря/DSS(DSS dictionary). - Opaque CMS — «совмещённая» подпись: PKCS#7
signedData, в котором подписываемый документ зашит внутри (encapContentInfo.eContent, RFC 5652 §5.2). Такой формат выдаёт Госключ при подписании PDF: файл может быть названdoc.pdf, но его байты — это CMS-обёртка, а сам PDF лежит внутри контейнера. Сервис автоматически распознаёт opaque CMS у любого расширения, извлекает встроенный документ и верифицирует подпись против него (отдельный файл-документ не требуется). В отчёте «имя файла подписи» получает синтетическое значение«{name} (opaque CMS)», а «имя файла документа» — фактическое имя загруженного файла. - S/MIME — почтовые сообщения
multipart/signedи opaqueapplication/pkcs7-mime. Расширения:.eml,.p7m,.mime,.msg. - XMLDSig / XAdES — XML-файлы (
.xml) с алгоритмами ГОСТ Р 34.10-2001/2012 (256/512) или RSA/ECDSA. Поддерживаются преобразованияc14n,exc-c14n,enveloped-signature,base64, W3C-1999 XPath и W3C-2002 XPath-Filter 2, а также проприетарныеurn://smev-gov-ru/xmldsig/transform(СМЭВ-3) иurn:xml-dsig:transformation:v1.1(ФТС таможня — частично, для мульти-namespace субдеревьев см. Известные ограничения в общей документации). Принимаются обе URI-семьи алгоритмов:urn:ietf:params:xml:ns:cpxmlsec:algorithms:*(CryptoPro xmlsec) иhttp://www.w3.org/2001/04/xmldsig-more#*(RFC 4490). - OOXML —
.docx,.xlsx,.pptxс поддержкойRelationshipTransformпо ECMA-376 п.13. - ZIP-архивы —
.zipраспаковывается на один уровень: документы и подписи внутри сопоставляются по содержимому (хеш) и по имени. Также поддерживается «архив подписан целиком»:foo.zip+foo.zip.sigлибоarchive.zipс сиблингом.sig, чейmessageDigestсовпадает с хешем архива.
Версионирование
Канонические пути API располагаются под префиксом /api/v{N}/. Контракт v1 зафиксирован: в существующие ответы могут добавляться новые необязательные поля, но имена и типы существующих полей не изменяются и не удаляются. Несовместимое изменение (переименование поля, изменение типа, удаление секции ответа) выпускается параллельной версией /api/v2; /api/v1 продолжает работать в прежнем виде.
Незаверсионированные пути /api/verify и /api/cert/check — это псевдонимы, ведущие на текущую актуальную версию. Они сохраняются для обратной совместимости со старыми интеграциями. В новых интеграциях используйте только версионированные пути, чтобы зафиксировать контракт.
Аутентификация
Доступ к API авторизуется по токену. Токен выдаётся при подключении и передаётся в заголовке Authorization: Bearer <token>. Запросы без валидного токена отклоняются с кодом 401 Unauthorized.
Все запросы выполняются по HTTPS. Максимальный размер тела запроса — 200 МБ.
Коды ошибок
| HTTP | Когда | Что вернётся |
|---|---|---|
| 200 OK | Запрос обработан. Невалидная подпись — это штатный результат проверки, статус по каждому файлу возвращается в items[i].status. | JSON: VerificationResponse или CertificateCheckResponse |
| 400 Bad Request | Запрос не в формате multipart/form-data, отсутствует поле с файлами или повреждена форма. | { "error": "..." } |
| 413 Payload Too Large | Превышен один из лимитов: размер запроса, размер записи в архиве, число записей, коэффициент сжатия (защита от zip-bomb). | { "error": "..." } с указанием конкретного лимита |
| 500 Internal Server Error | Внутренняя ошибка сервиса. | { "error": "..." } |
items[i].isValid и items[i].status.POST /api/v1/verify
Проверяет одну или несколько связок «документ + подпись». Принимает ZIP-архивы (распаковываются рекурсивно), пары вида «doc.pdf + doc.pdf.sig», PDF со встроенной PAdES-подписью, opaque-CMS файлы (документ зашит в encapContentInfo — типичный выхлоп Госключа), .eml с S/MIME, файлы .docx, .xlsx, .pptx и .xml (XMLDSig).
Про revocation в ответе. CRL-проверка делается поиском в CRL-банке в памяти — никаких сетевых запросов в момент проверки. Если CDP-URL сертификата ещё не проиндексирован банком (первая встреча УЦ), проверка revocation-crl приходит со статусом Skipped и пометкой «CRL not yet indexed»; URL автоматически кладётся в очередь фоновой прокачки, следующая проверка отдаст Good/Revoked. OCSP по умолчанию асинхронный: revocation-ocsp приходит как Pending, клиент обращается к /api/v1/revocation и подменяет результат. OCSP сразу в ответе — флаг ?withRevocation=true (медленнее на 0.5–10 с).
Параметры запроса (query или поле формы)
| Поле | Тип | Описание |
|---|---|---|
| files | file[] | Один или несколько файлов. Документ и подпись могут передаваться отдельными частями формы либо одним ZIP-архивом. Имя поля формы — files. |
| returnSignatureFile | bool | Если true, в каждый элемент результата добавляется signatureFileBase64 — байты файла подписи в base64 (для отделённой подписи), внутренний CMS (для PAdES, S/MIME и opaque CMS — здесь это байты самого загруженного файла) либо файл sig*.xml (для OOXML). Значение по умолчанию — false. |
| returnCertificateFile | bool | Если true, в каждом подписанте возвращается поле certificateBase64 с DER-представлением сертификата подписанта в base64. Значение по умолчанию — false. |
Пример запроса
curl -X POST 'https://api.cleanvoice.ru/cleansign/api/v1/verify?returnCertificateFile=true' \
-F 'files=@invoice.pdf' \
-F 'files=@invoice.pdf.sig' \
-H 'Authorization: Bearer YOUR_KEY'
import requests
files = [
('files', ('invoice.pdf', open('invoice.pdf', 'rb'))),
('files', ('invoice.pdf.sig', open('invoice.pdf.sig', 'rb'))),
]
resp = requests.post(
'https://api.cleanvoice.ru/cleansign/api/v1/verify',
params={'returnCertificateFile': 'true'},
files=files,
headers={'Authorization': 'Bearer YOUR_KEY'},
)
data = resp.json()
print(data['summary']['validlySigned'], 'valid of', data['summary']['totalDocuments'])
const fd = new FormData();
fd.append('files', pdfFile, 'invoice.pdf');
fd.append('files', sigFile, 'invoice.pdf.sig');
const r = await fetch('https://api.cleanvoice.ru/cleansign/api/v1/verify?returnCertificateFile=true', {
method: 'POST',
body: fd,
});
const data = await r.json();
console.log(data.summary.validlySigned + ' / ' + data.summary.totalDocuments);
using var http = new HttpClient { BaseAddress = new Uri("https://api.cleanvoice.ru/cleansign/") };
http.DefaultRequestHeaders.Authorization = new("Bearer", "YOUR_KEY");
using var form = new MultipartFormDataContent();
form.Add(new ByteArrayContent(File.ReadAllBytes("invoice.pdf")), "files", "invoice.pdf");
form.Add(new ByteArrayContent(File.ReadAllBytes("invoice.pdf.sig")), "files", "invoice.pdf.sig");
var resp = await http.PostAsync("api/v1/verify?returnCertificateFile=true", form);
var json = await resp.Content.ReadAsStringAsync();
Пример ответа (200 OK)
{
"summary": {
"totalDocuments": 1,
"signed": 1,
"validlySigned": 1,
"invalidSignatures": 0,
"notSigned": 0,
"orphanSignatures": 0
},
"items": [
{
"documentFile": "invoice.pdf",
"documentSize": 182734,
"signatureFile": "invoice.pdf.sig",
"isSigned": true,
"isValid": true,
"status": "Valid",
"documentHashAlgorithm": "GOST R 34.11-2012 (256)",
"documentHashHex": "8E5C…",
"messageDigestMatches": true,
"isPowerOfAttorney": false,
"signers": [
{
"subjectCommonName": "Иванов Иван Иванович",
"subjectType": "ЮЛ",
"signerShortName": "Иванов И. И.",
"signingTime": "2026-04-30T17:42:11+00:00",
"signatureValid": true,
"trustStatus": "Trusted",
"chainTrusted": true,
"trustedRootSubject": "CN=Минцифры России",
"classification": { "kind": "КЭП", "kindFull": "Квалифицированная электронная подпись",
"classes": ["КС1"], "subjectSignTool": "КриптоПро CSP (5.0.12000)",
"usesGost": true, "issuedByAccreditedUc": true, "isGoskey": false,
"deprecationWarnings": [] },
"attributes": { "inn": "772372861423", "innLe": "7709000010",
"ogrn": "1047709098315", "snils": "14233464635",
"email": "i.ivanov@example.com",
"givenName": "Иван Иванович", "surname": "Иванов",
"organization": "ООО «Пример»" },
"accreditation": { "issuingUcSki": "23F0DA4A5DE30C96E91F976A3E641689A1F8553C",
"issuingUcName": "УЦ ФНС России",
"accreditedFrom": "2024-01-01T00:00:00+00:00",
"accreditedTo": null },
// null accreditedTo → УЦ accredited at the moment of the response.
// null issuingUcSki → issuing УЦ wasn't found in the Минцифры registry.
// Clients derive "accredited at signing time" by comparing
// signingTime against AccreditedFrom / AccreditedTo themselves.
"certificateBase64": "MIIH…" // только если returnCertificateFile=true
}
],
"errors": [],
"warnings": []
}
]
}
POST /api/v1/cert/check
Проверка только сертификата, без документа. Эндпоинт оптимизирован для регулярных запусков из cron. Принимает .cer, .crt, .pem, .der или CMS-файл (.sig); во втором случае сертификат подписанта извлекается по SignerID. Возвращает срок действия, число дней до истечения, аккредитацию УЦ, статус OCSP и CRL, классификацию.
Пример: ежедневный мониторинг
#!/bin/bash
# Завершиться с ошибкой, если до истечения сертификата осталось менее 30 дней.
RESP=$(curl -sf -F files=@/etc/ssl/edo.cer https://api.cleanvoice.ru/cleansign/api/v1/cert/check)
DAYS=$(echo "$RESP" | jq '.items[0].daysUntilExpiry')
[ "$DAYS" -lt 30 ] && echo "ALERT: cert expires in $DAYS days" && exit 1
Пример ответа
{
"summary": { "total": 1, "currentlyValid": 1, "expiringSoon": 0, "expired": 0,
"trusted": 1, "untrusted": 0, "revoked": 0 },
"items": [{
"fileName": "edo.cer",
"source": "X509",
"subjectCommonName": "ООО «Пример»",
"issuer": "CN=Удостоверяющий центр Федерального казначейства",
"notBefore": "2025-08-12T08:14:00+00:00",
"notAfter": "2026-08-12T08:14:00+00:00",
"isCurrentlyValid": true,
"daysUntilExpiry": 97,
"publicKeyAlgorithm":"GOST R 34.10-2012 (256)",
"classification": { "kind": "КЭП", "classes": ["КС1"], "issuedByAccreditedUc": true },
"trust": { "status": "Trusted", "trusted": true,
"rootSubject": "CN=Минцифры России",
"chain": [...] },
"accreditation": { "issuingUcSki": "...", "issuingUcName": "УЦ Казначейства",
"accreditedFrom": "2024-01-01T00:00:00+00:00", "accreditedTo": null },
"revocation": null,
"revocationCrl": null
}]
}
Асинхронная OCSP-проверка сертификата
Эндпоинт делает OCSP-запрос к responder'у УЦ (RFC 6960) для одного сертификата. Спроектирован как асинхронное продолжение /api/v1/verify и /api/v1/cert/check: по умолчанию они откладывают OCSP-запрос (он может занять до нескольких секунд) и возвращают check со статусом Pending и URL OCSP-endpoint'а. Клиент (фронт или CLI) дёргает этот эндпоинт с DER-сертификатом подписанта и получает финальный статус.
CRL revocation через этот эндпоинт не запрашивается. Начиная с версии с CRL-банком, CRL-проверка делается сразу в ответе /verify и /cert/check по индексу отозванных серийников в памяти (см. /api/v1/crl-bank/stats) — это поиск за константное время без обращений по сети. Здесь только OCSP, который остаётся онлайн-запросом к серверу УЦ.
Сертификат-издатель резолвится из TrustStore по SubjectKeyIdentifier / IssuerDN. Если издателя нет в trust-store (зарубежные / тестовые УЦ — EJBCA, Let's Encrypt, DigiCert), check возвращается как NotApplicable с пометкой «issuer cert not in trust store» — без открытого ключа издателя OCSP-запрос построить нельзя.
Без rate-limit: UI стреляет в этот эндпоинт параллельно для каждого подписанта в multi-signer документе. Bearer-токен остаётся обязательным.
Запрос
POST /api/v1/revocation
Content-Type: application/json
Authorization: Bearer <session-token>
{
"signerCertBase64": "MIIH..."
}
Параметры тела
| Поле | Тип | Описание |
|---|---|---|
| signerCertBase64 | string | DER-сертификат подписанта в base64. Обычно копируется из ответа /verify — поле items[].signers[].certificateBase64. PEM-обёртку и пробелы убирать не нужно — парсер их игнорирует. |
Ответ — OCSP прошёл
200 OK
{
"ocsp": {
"status": "Good",
"producedAt": "2026-05-16T19:49:55+00:00",
"thisUpdate": "2026-05-16T19:49:55+00:00",
"ocspUrl": "http://ocsp1.taxcom.ru/OCSPAL/ocsp.srf"
},
"ocspCheck": {
"id": "revocation-ocsp",
"status": "Ok",
"details": "Endpoint: http://ocsp1.taxcom.ru/..."
}
}
Поля ответа
| Поле | Тип | Описание |
|---|---|---|
| ocsp | OcspResult? | Сырой результат OCSP-запроса (RFC 6960). null если OCSP отключён в конфиге или не было issuer cert. |
| ocsp.status | string | Good / Revoked / Unknown / NoEndpoint / FetchFailed / InvalidResponse. Это сырое значение по протоколу OCSP (не унифицированный VerificationCheck.Status). |
| ocsp.ocspUrl | string? | URL, по которому был отправлен OCSP-запрос (из id-ad-ocsp AIA-extension сертификата). |
| ocspCheck | VerificationCheck | Унифицированный check той же формы, что в /verify. Содержит id="revocation-ocsp", status из таксономии VerificationCheck (Ok / Failed / Unreachable / NotApplicable / Skipped), endpoint URL в details. Подходит для прямой замены Pending-чека из /verify по id. |
Ответ — сертификат отозван
200 OK
{
"ocsp": {
"status": "Revoked",
"revocationTime": "2025-08-12T11:14:00+00:00",
"revocationReason": "keyCompromise",
"ocspUrl": "http://pki.tax.gov.ru/ocsp02/ocsp.srf"
},
"ocspCheck": {
"id": "revocation-ocsp",
"status": "Failed",
"critical": true,
"details": "Revoked at 2025-08-12T11:14:00Z, reason: keyCompromise. Endpoint: http://pki.tax.gov.ru/..."
}
}
Ответ — issuer не в trust-store (зарубежный/тестовый УЦ)
200 OK
{
"ocsp": null,
"ocspCheck": {
"id": "revocation-ocsp",
"status": "NotApplicable",
"details": "Cannot check OCSP: the signer's issuer certificate is not in the trust store..."
}
}
Для зарубежных УЦ (EJBCA, Let's Encrypt, DigiCert и т.п.) — это ожидаемый ответ. Чтобы OCSP заработал, добавьте сертификат-издатель в trust-store/intermediates/.
Ответ — OCSP выключен в конфиге
200 OK
{
"ocsp": null,
"ocspCheck": {
"id": "revocation-ocsp",
"status": "Skipped",
"details": "Disabled in configuration (CleanSign:TrustStore:EnableOcsp=false)."
}
}
Использование из браузера
// Достали cert из ответа /verify
const cert = response.items[0].signers[0].certificateBase64;
// Дёрнули revocation
const r = await fetch('/api/v1/revocation', {
method: 'POST',
headers: {
'Authorization': 'Bearer ' + token,
'Content-Type': 'application/json',
},
body: JSON.stringify({ signerCertBase64: cert }),
});
const { ocspCheck } = await r.json();
// Заменили Pending-чек на финальный
signer.checks = signer.checks.map(c =>
c.id === ocspCheck.id ? ocspCheck : c);
Inline-альтернатива для бизнес-API
Если асинхронная схема неудобна (например, скрипт по расписанию) — можно попросить /verify вернуть OCSP сразу в ответе:
POST /api/v1/verify?withRevocation=true
Content-Type: multipart/form-data
...