Подписанные операции DID

Создание, изменение и деактивация DID Document принимаются только с подписью контроллера или владельца, указанных в документе. Каждая ревизия хранит подпись и звено хэш-цепочки, поэтому историю DID можно перепроверить: правка ревизии в базе обнаруживается.

Кто вправе подписать

Поле документа

Смысл

controller

DID (строка или массив), которые управляют документом. Не задан — документом управляет сам DID

owner

DID организации, от имени которой действует субъект (ИИ-агент, устройство, персона). Подписывает наравне с контроллером

capabilityInvocation

ключи, которыми подписант может менять DID-документы. Если отношение у подписанта не задано, подходит ключ из authentication

Полномочия проверяются по текущей сохранённой версии документа (для создания — по создаваемому документу). Поэтому изменение не может само вписать нового контроллера и тут же им подписаться.

Согласие владельца. Владелец отвечает за субъекта, поэтому назначить owner (при создании или изменении) может только подпись самого этого владельца. Самоподписанный DID не может объявить организацию своим владельцем. Передача владения — в два шага: текущий контроллер или владелец добавляет новую организацию в controller, затем новая организация своей подписью устанавливает себя в owner (и при желании убирает себя из controller).

Ключ подписанта берётся из его DID Document: собственный — из того же документа, ключ контроллера или владельца — из реестра. Деактивированный DID подписывать не может.

Режимы

Переменная DID_OPERATION_PROOF:

Значение

Поведение

off

подписи не проверяются

optional (по умолчанию)

подпись есть — проверяется строго; подписи нет — операция проходит, ревизия помечается как неподписанная

required

операция без действительной подписи отклоняется

В режиме optional неподписанная операция не проверяется вовсе — в том числе на полномочия и на согласие владельца. Гарантии этого сценария действуют только для подписанных операций, полностью — в режиме required.

Формат подписи

Заголовок DID-Operation-Proof — compact JWS (RFC 7515):

  • protected header: alg (EdDSA, ES256, ES384, ES512, RS256, PS256), kid — DID URL ключа подписанта (did:example:org#key-1), typ: did-operation+jws;

  • payload:

{ "op": "update", "did": "did:ru:agent:z7iAK46JKsNfPgWkUErXXhtWXH7827MKHqRWeZnmA19C1", "prevVersionId": "1", "bodyDigest": "<base64url SHA-256 точных байт тела запроса>", "iat": 1792105200 }

Операция

HTTP

op

prevVersionId

создание

POST /v1/dids

create

не передаётся

замена

PUT /v1/dids/{did}

update

текущий versionId

изменение

PATCH /v1/dids/{did}

update

текущий versionId

деактивация

PATCH /v1/dids/{did} с didDocumentMetadata.deactivated=true

deactivate

текущий versionId

Подпись привязана к DID, к версии и к телу запроса. Повтор той же подписи после применения даёт 409 (версия уже другая), подпись старше 5 минут (iat) отклоняется.

Самосертифицируемые DID

DID метода did:ru (did:ru:agent|device|org:<id>, спецификация) всегда самосертифицируемые: <id> = z + base58btc от SHA-256 канонического JWK (RFC 7638) первого ключа capabilityInvocation начального документа. Такой DID нельзя занять, не имея ключа. У агента и устройства обязателен owner. Готовый DID печатает bin/didsign keygen -type agent.

То же правило можно включить для других префиксов: DID_SELF_CERTIFYING_PREFIXES (через запятую, например did:scada:device:; по умолчанию пусто).

Утилита didsign

go build -o bin/didsign ./cmd/didsign bin/didsign keygen # seed (секрет) и публичный JWK для verificationMethod bin/didsign keygen -type agent # + DID did:ru:agent:…, выведенный из ключа bin/didsign sign -seed <seed> -kid did:example:org#key-1 \ -op create -did $AGENT_DID -body agent.json # значение заголовка

Подписывается файл с телом ровно в том виде, в каком он уйдёт в запрос (curl --data-binary @file).

Сценарий: организация регистрирует ИИ-агента

Сервис запущен с DID_OPERATION_PROOF=required, B=http://localhost:8080/api/v1.

  1. Ключи организации и агента: bin/didsign keygen > org.key, bin/didsign keygen > agent.key.

  2. Документ организации org.json с ключом did:example:org#key-1 в verificationMethod, authentication и capabilityInvocation.

  3. Без подписи:

curl -X POST $B/dids -H 'Content-Type: application/json' --data-binary @org.json # 401, WWW-Authenticate: DID-Operation-Proof
  1. Организация подписывает свой документ:

P=$(bin/didsign sign -seed $ORG_SEED -kid did:example:org#key-1 -op create -did did:example:org -body org.json) curl -X POST $B/dids -H 'Content-Type: application/json' -H "DID-Operation-Proof: $P" --data-binary @org.json # 201
  1. bin/didsign keygen -type agent > agent.key, AGENT_DID — строка did: из вывода. Документ агента agent.json с "id": "$AGENT_DID", "owner": "did:example:org" и собственным ключом в assertionMethod и capabilityInvocation. Создание подписывает организация-владелец:

P=$(bin/didsign sign -seed $ORG_SEED -kid did:example:org#key-1 -op create -did $AGENT_DID -body agent.json) curl -X POST $B/dids -H 'Content-Type: application/json' -H "DID-Operation-Proof: $P" --data-binary @agent.json # 201
  1. Агент добавляет себе service endpoint (patch.json), подписывая своим ключом:

P=$(bin/didsign sign -seed $AGENT_SEED -kid $AGENT_DID#key-1 -op update -did $AGENT_DID -prev 1 -body patch.json) curl -X PATCH "$B/dids/$AGENT_DID" -H 'Content-Type: application/merge-patch+json' \ -H "DID-Operation-Proof: $P" --data-binary @patch.json # 200; повтор того же запроса — 409

Та же подпись, сделанная чужим ключом с kid организации, — 401 invalid did operation proof: invalid JWS signature.

  1. Организация деактивирует агента (deact.json = {"didDocumentMetadata":{"deactivated":true}}, -op deactivate -prev 2) — 200.

  2. Проверка истории:

curl "$B/dids/$AGENT_DID/history/verify"
valid: true 1 create verified did:example:org 2 update verified $AGENT_DID 3 deactivate verified did:example:org
  1. Если изменить ревизию 2 прямо в базе (UPDATE did_revisions SET document = ...), проверка истории вернёт valid: false и для ревизии 2 — broken с причинами digest does not match the stored revision и stored document differs from the signed request.

Проверка истории

GET /v1/dids/{did}/history/verify для каждой ревизии:

  • пересчитывает digest и сверяет звено prevDigest с предыдущей ревизией;

  • проверяет подпись ключом подписанта в той версии его документа, что действовала на момент операции (поздняя ротация ключа старые подписи не ломает);

  • восстанавливает документ из сохранённого подписанного тела запроса и сравнивает с сохранённым — ревизию не подделать, даже пересчитав digest.

Статус

Значение

verified

digest, звено цепочки и подпись верны

unsigned

цепочка верна, операция принята без подписи (режим optional)

legacy

ревизия записана до появления цепочки

broken

ревизия изменена после записи или подпись не проходит; valid: false

headDigest — digest последней ревизии: значение для закрепления в HLF.

Чего проверка внутри сервиса не ловит: подмену неподписанных ревизий целиком и обрезку хвоста истории. Это закрывает закрепление headDigest в реестре HLF.

Коды ответов

Код

Когда

401

нет подписи в режиме required; подпись не разбирается, не сходится или устарела; bodyDigest не совпадает с телом

403

подписант не контроллер и не владелец; новый owner не подписал операцию; DID подписанта не зарегистрирован или деактивирован; ключ не в capabilityInvocation

409

подпись сделана для другой версии документа

Identity-сервис

С IDENTITY_SYSTEM_KEY (base64url seed Ed25519, секрет) identity-сервис публикует этот ключ в системном DID, становится owner DID персон и подписывает их создание. Без переменной DID персон регистрируются без подписи — это работает только в режимах off и optional.

Алгоритмы ГОСТ

Подписи ГОСТ Р 34.10-2012 подключаются через didproof.Verifiers.Register с проверкой на стороне PKI-сервиса; в стандартной поставке их нет.

Журнал изменений

Дата

Перечень изменений

Автор

2026-11-05

Перенесён из архива DID-VC-итоги-2026-11-03 без изменения содержания.

Дис