CleanSign API

Справочник · v1

CleanSign API

REST-API сервиса CleanSign для проверки электронных подписей (ЭП/ЭЦП). Принимает документы и файлы подписи через multipart/form-data, возвращает JSON со сведениями о подписантах, цепочкой сертификатов и классификацией подписи. Использует тот же движок проверки, что и веб-форма на главной странице. Подходит для пакетной обработки документов и мониторинга срока действия сертификатов; готовых коннекторов к учётным системам и операторам ЭДО сервис не поставляет — встраивание выполняется на стороне клиента.

Базовый URL: 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 и opaque application/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": "..." }
Важно: отрицательный результат проверки подписи возвращается с кодом 200 OK, а не 4xx. Сам запрос обработан корректно; результат проверки находится в полях 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 с).

POST /api/v1/verify
multipart/form-data → application/json

Параметры запроса (query или поле формы)

ПолеТипОписание
filesfile[]Один или несколько файлов. Документ и подпись могут передаваться отдельными частями формы либо одним ZIP-архивом. Имя поля формы — files.
returnSignatureFileboolЕсли true, в каждый элемент результата добавляется signatureFileBase64 — байты файла подписи в base64 (для отделённой подписи), внутренний CMS (для PAdES, S/MIME и opaque CMS — здесь это байты самого загруженного файла) либо файл sig*.xml (для OOXML). Значение по умолчанию — false.
returnCertificateFileboolЕсли 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, классификацию.

POST /api/v1/cert/check
multipart/form-data → application/json

Пример: ежедневный мониторинг

#!/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
  }]
}
POST/api/v1/revocation

Асинхронная 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..."
}

Параметры тела

ПолеТипОписание
signerCertBase64stringDER-сертификат подписанта в 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.statusstringGood / Revoked / Unknown / NoEndpoint / FetchFailed / InvalidResponse. Это сырое значение по протоколу OCSP (не унифицированный VerificationCheck.Status).
ocsp.ocspUrlstring?URL, по которому был отправлен OCSP-запрос (из id-ad-ocsp AIA-extension сертификата).
ocspCheckVerificationCheckУнифицированный 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
...