Узнайте, как создать эмитент верифицируемых удостоверений W3C с использованием протокола OpenID4VCI. В этом пошаговом руководстве мы покажем, как создать приложение на Next.js, которое выпускает криптографически подписанные удостоверения, совместимые с ци
Amine
Created: August 20, 2025
Updated: August 21, 2025
See the original blog version in English here.
Цифровые удостоверения — это мощный способ подтверждения личности и заявлений безопасным и конфиденциальным образом. Но как пользователи получают эти удостоверения? Именно здесь решающую роль играет эмитент. Эмитент — это доверенная организация, например государственное учреждение, университет или банк, которая отвечает за создание и выдачу пользователям удостоверений с цифровой подписью.
Это руководство представляет собой подробное пошаговое пособие по созданию эмитента цифровых удостоверений. Мы сосредоточимся на протоколе OpenID for Verifiable Credential Issuance (OpenID4VCI) — современном стандарте, который определяет, как пользователи могут получать удостоверения от эмитента и безопасно хранить их в своих цифровых кошельках.
В результате мы получим работающее приложение на Next.js, которое сможет:
Recent Articles
📝
Как создать верификатор цифровых учетных данных (руководство для разработчиков)
📝
Как создать эмитент цифровых удостоверений (руководство для разработчиков)
📖
Резидентный ключ WebAuthn: обнаруживаемые учетные данные как Passkeys
🔑
Доступ по физическим пропускам и Passkeys: техническое руководство
🔑
Внедрение обязательной MFA и переход на Passkeys: лучшие практики
Прежде чем продолжить, важно прояснить различие между двумя связанными, но разными понятиями:
Цифровые удостоверения (общий термин): Это широкая категория, которая охватывает любую цифровую форму удостоверений, сертификатов или аттестаций. Это могут быть простые цифровые сертификаты, базовые цифровые значки или любые удостоверения, хранящиеся в электронном виде, которые могут иметь или не иметь криптографических средств защиты.
Верифицируемые удостоверения (VC — стандарт W3C): Это особый тип цифровых удостоверений, который соответствует стандарту W3C Verifiable Credentials Data Model. Верифицируемые удостоверения — это криптографически подписанные, защищенные от подделки и сохраняющие конфиденциальность удостоверения, которые можно проверить независимо. Они включают в себя особые технические требования, такие как:
В этом руководстве мы создаем именно эмитент верифицируемых удостоверений, который соответствует стандарту W3C, а не просто любую систему цифровых удостоверений. Протокол OpenID4VCI, который мы используем, специально разработан для выпуска верифицируемых удостоверений, а формат JWT-VC, который мы будем реализовывать, является совместимым со стандартом W3C форматом для верифицируемых удостоверений.
Магия цифровых удостоверений заключается в простой, но мощной модели «треугольника доверия», в которой участвуют три ключевых игрока:
Процесс выпуска — это первый шаг в этой экосистеме. Эмитент проверяет информацию пользователя и предоставляет ему удостоверение. Как только держатель получает это удостоверение в свой кошелек, он может предъявить его верификатору, чтобы подтвердить свою личность или заявления, замыкая треугольник.
Вот краткий обзор работы готового приложения:
Шаг 1: Ввод данных пользователя Пользователь заполняет форму со своими личными данными, чтобы запросить новое удостоверение.
Шаг 2: Генерация предложения удостоверения Приложение генерирует безопасное предложение удостоверения, отображаемое в виде QR-кода и предварительно авторизованного кода.
Шаг 3: Взаимодействие с кошельком Пользователь сканирует QR-код совместимым кошельком (например, Sphereon Wallet) и вводит PIN-код для авторизации выпуска.
Шаг 4: Удостоверение выпущено Кошелек получает и сохраняет только что выпущенное цифровое удостоверение, готовое к использованию в будущем.
Прежде чем мы углубимся в код, давайте рассмотрим основные знания и инструменты, которые нам понадобятся. Это руководство предполагает, что у вас есть базовое знакомство с концепциями веб-разработки, но следующие предварительные требования необходимы для создания эмитента удостоверений.
Наш эмитент построен на наборе открытых стандартов, которые обеспечивают совместимость между кошельками и сервисами выпуска. В этом руководстве мы сосредоточимся на следующих:
Стандарт / Протокол | Описание |
---|---|
OpenID4VCI | OpenID for Verifiable Credential Issuance. Это основной протокол, который мы будем использовать. Он определяет стандартный процесс, с помощью которого пользователь (через свой кошелек) может запросить и получить удостоверение от эмитента. |
JWT-VC | Верифицируемые удостоверения на основе JWT. Формат удостоверения, которое мы будем выпускать. Это стандарт W3C, который кодирует верифицируемые удостоверения как JSON Web Tokens (JWT), делая их компактными и удобными для использования в вебе. |
ISO mDoc | ISO/IEC 18013-5. Международный стандарт для мобильных водительских удостоверений (mDL). Хотя мы выпускаем JWT-VC, заявления (claims) внутри него структурированы так, чтобы быть совместимыми с моделью данных mDoc (например, eu.europa.ec.eudi.pid.1 ). |
OAuth 2.0 | Базовая платформа авторизации, используемая OpenID4VCI. Мы реализуем поток pre-authorized_code , который является специфическим типом предоставления прав, разработанным для безопасного и удобного выпуска удостоверений. |
OpenID4VCI поддерживает два основных потока авторизации для выпуска удостоверений:
Поток с предварительно авторизованным кодом (Pre-Authorized Code Flow): В этом
потоке эмитент генерирует кратковременный, одноразовый код (pre-authorized_code
),
который сразу же доступен пользователю. Кошелек пользователя может затем обменять этот
код непосредственно на удостоверение. Этот поток идеально подходит для сценариев, когда
пользователь уже аутентифицирован и находится на веб-сайте
эмитента, поскольку он обеспечивает бесшовный, мгновенный выпуск
без перенаправлений.
Поток с кодом авторизации (Authorization Code Flow): Это стандартный поток
OAuth 2.0, при котором пользователь перенаправляется на сервер
авторизации для предоставления согласия. После одобрения сервер отправляет
authorization_code
обратно на зарегистрированный redirect_uri
. Этот поток больше
подходит для сторонних приложений, которые инициируют процесс выпуска от имени
пользователя.
В этом руководстве мы будем использовать поток pre-authorized_code
. Мы выбрали этот
подход, потому что он проще и обеспечивает более прямой пользовательский опыт для нашего
конкретного случая: пользователь напрямую запрашивает удостоверение с собственного
веб-сайта эмитента. Это устраняет необходимость в сложных
перенаправлениях и регистрации клиента, делая основную логику выпуска проще для понимания
и реализации.
Эта комбинация стандартов позволяет нам создать эмитент, совместимый с широким спектром цифровых кошельков и обеспечивающий безопасный, стандартизированный процесс для пользователя.
Для создания нашего эмитента мы будем использовать тот же надежный и современный технологический стек, который мы использовали для верификатора, обеспечивая последовательный и высококачественный опыт для разработчиков.
Мы будем использовать TypeScript как для фронтенда, так и для бэкенда. Его статическая типизация неоценима в критически важном для безопасности приложении, таком как эмитент, поскольку она помогает предотвратить распространенные ошибки и улучшает общее качество и поддерживаемость кода.
Next.js — наш выбор фреймворка, потому что он обеспечивает бесшовный, интегрированный опыт для создания full-stack приложений.
Наша реализация будет опираться на несколько ключевых библиотек для решения конкретных задач:
pre-authorized_code
.Для тестирования вашего эмитента вам понадобится мобильный кошелек, поддерживающий протокол OpenID4VCI. Для этого руководства мы рекомендуем Sphereon Wallet, который доступен как для Android, так и для iOS.
Как установить Sphereon Wallet:
Выпуск удостоверения — это критически важная для безопасности операция, которая опирается на фундаментальные криптографические концепции для обеспечения доверия и подлинности.
По своей сути, верифицируемое удостоверение — это набор заявлений, который был подписан цифровой подписью эмитента. Эта подпись предоставляет две гарантии:
Цифровые подписи создаются с использованием криптографии с открытым и закрытым ключами. Вот как это работает:
В нашей реализации мы сгенерируем пару ключей на эллиптических кривых (EC) и будем
использовать алгоритм ES256
для подписи JWT-VC. Открытый ключ встроен в DID эмитента
(did:web
), что позволяет любому верификатору обнаружить его и проверить подпись
удостоверения. Примечание: Заявление aud
(audience) намеренно опущено в наших JWT,
поскольку удостоверение предназначено для общего использования и не привязано к
конкретному кошельку. Если вы хотите ограничить использование определенной аудиторией,
включите заявление aud
и установите его соответствующим образом.
Наше приложение-эмитент построено как
full-stack проект на Next.js с четким
разделением между логикой фронтенда и бэкенда. Эта архитектура позволяет нам создать
бесшовный пользовательский опыт, обрабатывая все критически важные для безопасности
операции на сервере. Важно: Включенные в SQL таблицы verification_sessions
и
verified_credentials
не требуются для этого эмитента, но добавлены для полноты картины.
src/app/issue/page.tsx
): Одна страница на React,
которая позволяет пользователям вводить свои данные для запроса удостоверения. Она
делает API-вызовы на наш бэкенд для инициации процесса выпуска.src/app/api/issue/...
): Набор серверных эндпоинтов, которые
реализуют протокол OpenID4VCI.
/.well-known/openid-credential-issuer
: Публичный эндпоинт метаданных. Это первый
URL, который кошелек проверит, чтобы узнать о возможностях эмитента, включая его
сервер авторизации, эндпоинт токенов, эндпоинт удостоверений и типы предлагаемых
удостоверений./.well-known/openid-configuration
: Стандартный эндпоинт обнаружения OpenID
Connect. Хотя он тесно связан с предыдущим, этот эндпоинт предоставляет более
широкую конфигурацию, связанную с OIDC, и часто требуется для совместимости со
стандартными клиентами OpenID./.well-known/did.json
: DID-документ нашего эмитента. При использовании метода
did:web
этот файл используется для публикации открытых ключей эмитента, которые
верификаторы могут использовать для проверки подписей выпускаемых им удостоверений.authorize/route.ts
: Создает pre-authorized_code
и предложение удостоверения.token/route.ts
: Обменивает pre-authorized_code
на
токен доступа.credential/route.ts
: Выпускает окончательное, криптографически подписанное JWT-VC.schemas/pid/route.ts
: Предоставляет JSON-схему для удостоверения PID. Это
позволяет любому потребителю удостоверения понять его структуру и типы данных.src/lib/
):
database.ts
: Управляет всеми взаимодействиями с базой данных, такими как хранение
кодов авторизации и ключей эмитента.crypto.ts
: Обрабатывает все криптографические операции, включая генерацию ключей и
подписание JWT.Вот диаграмма, иллюстрирующая процесс выпуска:
Теперь, когда у нас есть четкое понимание стандартов, протоколов и архитектуры, мы можем приступить к созданию нашего эмитента.
Следуйте инструкциям или используйте готовый код
Сейчас мы пошагово пройдем через настройку и реализацию кода. Если вы предпочитаете сразу перейти к готовому продукту, вы можете склонировать полный проект из нашего репозитория на GitHub и запустить его локально.
git clone https://github.com/corbado/digital-credentials-example.git
Сначала мы инициализируем новый проект Next.js, установим необходимые зависимости и запустим нашу базу данных.
Откройте терминал, перейдите в каталог, где вы хотите создать свой проект, и выполните следующую команду. Мы используем App Router, TypeScript и Tailwind CSS для этого проекта.
npx create-next-app@latest . --ts --eslint --tailwind --app --src-dir --import-alias "@/*" --use-npm
Эта команда создает новый скелет приложения Next.js в вашем текущем каталоге.
Далее нам нужно установить библиотеки, которые будут обрабатывать JWT, подключения к базе данных и генерацию UUID.
npm install jose mysql2 uuid @types/uuid
Эта команда устанавливает:
jose
: для подписи и проверки JSON Web Tokens (JWT).mysql2
: клиент MySQL для нашей базы данных.uuid
: для генерации уникальных строк вызова (challenge strings).@types/uuid
: типы TypeScript для библиотеки uuid
.Нашему бэкенду требуется база данных MySQL для
хранения кодов авторизации, сессий выпуска и ключей эмитента. Мы включили файл
docker-compose.yml
, чтобы упростить этот процесс.
Если вы склонировали репозиторий, вы можете просто выполнить docker-compose up -d
. Если
вы создаете проект с нуля, создайте файл с именем docker-compose.yml
со следующим
содержимым:
services: mysql: image: mysql:8.0 restart: always environment: MYSQL_ROOT_PASSWORD: rootpassword MYSQL_DATABASE: digital_credentials MYSQL_USER: app_user MYSQL_PASSWORD: app_password ports: - "3306:3306" volumes: - mysql_data:/var/lib/mysql - ./sql/init.sql:/docker-entrypoint-initdb.d/init.sql healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] timeout: 20s retries: 10 volumes: mysql_data:
Эта настройка Docker Compose также требует скрипта инициализации SQL. Создайте каталог с
именем sql
и внутри него файл с именем init.sql
со следующим содержимым для настройки
необходимых таблиц как для верификатора, так и для эмитента:
-- Create database if not exists CREATE DATABASE IF NOT EXISTS digital_credentials; USE digital_credentials; -- Table for storing challenges CREATE TABLE IF NOT EXISTS challenges ( id VARCHAR(36) PRIMARY KEY, challenge VARCHAR(255) NOT NULL UNIQUE, expires_at TIMESTAMP NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, used BOOLEAN DEFAULT FALSE, INDEX idx_challenge (challenge), INDEX idx_expires_at (expires_at) ); -- Table for storing verification sessions CREATE TABLE IF NOT EXISTS verification_sessions ( id VARCHAR(36) PRIMARY KEY, challenge_id VARCHAR(36), status ENUM('pending', 'verified', 'failed', 'expired') DEFAULT 'pending', presentation_data JSON, verified_at TIMESTAMP NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, FOREIGN KEY (challenge_id) REFERENCES challenges(id) ON DELETE CASCADE, INDEX idx_challenge_id (challenge_id), INDEX idx_status (status) ); -- Table for storing verified credentials data (optional) CREATE TABLE IF NOT EXISTS verified_credentials ( id VARCHAR(36) PRIMARY KEY, session_id VARCHAR(36), credential_type VARCHAR(255), issuer VARCHAR(255), subject VARCHAR(255), claims JSON, verified_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (session_id) REFERENCES verification_sessions(id) ON DELETE CASCADE, INDEX idx_session_id (session_id), INDEX idx_credential_type (credential_type) ); -- ISSUER TABLES -- Table for storing authorization codes in OpenID4VCI flow CREATE TABLE IF NOT EXISTS authorization_codes ( id VARCHAR(36) PRIMARY KEY, code VARCHAR(255) NOT NULL UNIQUE, client_id VARCHAR(255), scope VARCHAR(255), code_challenge VARCHAR(255), code_challenge_method VARCHAR(50), redirect_uri TEXT, user_pin VARCHAR(10), expires_at TIMESTAMP NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, used BOOLEAN DEFAULT FALSE, INDEX idx_code (code), INDEX idx_expires_at (expires_at) ); -- Table for storing issuance sessions CREATE TABLE IF NOT EXISTS issuance_sessions ( id VARCHAR(36) PRIMARY KEY, authorization_code_id VARCHAR(36), access_token VARCHAR(255), token_type VARCHAR(50) DEFAULT 'Bearer', expires_in INT DEFAULT 3600, c_nonce VARCHAR(255), c_nonce_expires_at TIMESTAMP, status ENUM('pending', 'authorized', 'credential_issued', 'expired', 'failed') DEFAULT 'pending', user_data JSON, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, FOREIGN KEY (authorization_code_id) REFERENCES authorization_codes(id) ON DELETE CASCADE, INDEX idx_access_token (access_token), INDEX idx_c_nonce (c_nonce), INDEX idx_status (status) ); -- Table for storing issued credentials CREATE TABLE IF NOT EXISTS issued_credentials ( id VARCHAR(36) PRIMARY KEY, session_id VARCHAR(36), credential_id VARCHAR(255), credential_type VARCHAR(255) DEFAULT 'jwt_vc', doctype VARCHAR(255) DEFAULT 'eu.europa.ec.eudi.pid.1', credential_data LONGTEXT, -- Base64 encoded mDoc credential_claims JSON, issuer_did VARCHAR(255), subject_id VARCHAR(255), issued_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, expires_at TIMESTAMP, revoked BOOLEAN DEFAULT FALSE, revoked_at TIMESTAMP NULL, FOREIGN KEY (session_id) REFERENCES issuance_sessions(id) ON DELETE CASCADE, INDEX idx_credential_id (credential_id), INDEX idx_session_id (session_id), INDEX idx_doctype (doctype), INDEX idx_subject_id (subject_id), INDEX idx_issued_at (issued_at) ); -- Table for storing issuer keys (simplified for demo) CREATE TABLE IF NOT EXISTS issuer_keys ( id VARCHAR(36) PRIMARY KEY, key_id VARCHAR(255) NOT NULL UNIQUE, key_type VARCHAR(50) NOT NULL, -- 'EC', 'RSA' algorithm VARCHAR(50) NOT NULL, -- 'ES256', 'RS256', etc. public_key TEXT NOT NULL, -- JWK format private_key TEXT NOT NULL, -- JWK format (encrypted in production) is_active BOOLEAN DEFAULT TRUE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, INDEX idx_key_id (key_id), INDEX idx_is_active (is_active) );
Когда оба файла будут на месте, откройте терминал в корне проекта и выполните:
docker-compose up -d
Эта команда запустит контейнер MySQL в фоновом режиме, готовый к использованию нашим приложением.
Прежде чем создавать API-эндпоинты, давайте создадим общие библиотеки, которые будут обрабатывать основную бизнес-логику. Такой подход позволяет нам держать наши API-маршруты чистыми и сфокусированными на обработке HTTP-запросов, в то время как сложная работа делегируется этим модулям.
src/lib/database.ts
)#Этот файл является единственным источником истины для всех взаимодействий с базой данных.
Он использует библиотеку mysql2
для подключения к нашему контейнеру MySQL и
предоставляет набор экспортируемых функций для создания, чтения и обновления записей в
наших таблицах. Этот уровень абстракции делает наш код более модульным и простым в
обслуживании.
Создайте файл src/lib/database.ts
со следующим содержимым:
// src/lib/database.ts import mysql from "mysql2/promise"; // Database connection configuration const dbConfig = { host: process.env.DATABASE_HOST || "localhost", port: parseInt(process.env.DATABASE_PORT || "3306"), user: process.env.DATABASE_USER || "app_user", password: process.env.DATABASE_PASSWORD || "app_password", database: process.env.DATABASE_NAME || "digital_credentials", timezone: "+00:00", }; let connection: mysql.Connection | null = null; export async function getConnection(): Promise<mysql.Connection> { if (!connection) { connection = await mysql.createConnection(dbConfig); } return connection; } // Data-Access-Object (DAO) functions for each table // ... (e.g., createChallenge, getChallenge, createAuthorizationCode, etc.)
Примечание: Для краткости полный список функций DAO был опущен. Вы можете найти полный код в репозитории проекта. Этот файл включает функции для управления challenges, сессиями верификации, кодами авторизации, сессиями выпуска и ключами эмитента.
src/lib/crypto.ts
)#Этот файл обрабатывает все критически важные для безопасности криптографические операции.
Он использует библиотеку jose
для генерации пар ключей и подписи JSON Web Tokens (JWT).
Генерация ключей Функция generateIssuerKeyPair
создает новую пару ключей на
эллиптических кривых, которая будет использоваться для подписи удостоверений. Открытый
ключ экспортируется в формате JSON Web Key (JWK), чтобы его можно было опубликовать в
нашем документе did.json
.
// src/lib/crypto.ts import { generateKeyPair, exportJWK, SignJWT } from "jose"; export async function generateIssuerKeyPair(keyId: string, issuerDid: string) { const { publicKey, privateKey } = await generateKeyPair("ES256", { crv: "P-256", extractable: true, }); const publicKeyJWK = await exportJWK(publicKey); publicKeyJWK.kid = keyId; // Assign a unique key ID // ... (private key export and other setup) return { publicKey, privateKey, publicKeyJWK /* ... */ }; }
Создание JWT-удостоверения Функция createJWTVerifiableCredential
является ядром
процесса выпуска. Она принимает заявления пользователя, пару ключей эмитента и другие
метаданные и использует их для создания подписанного JWT-VC.
// src/lib/crypto.ts export async function createJWTVerifiableCredential( claims: MDocClaims, issuerKeyPair: IssuerKeyPair, subjectId: string, audience: string, ): Promise<string> { const now = Math.floor(Date.now() / 1000); const oneYear = 365 * 24 * 60 * 60; const vcPayload = { // DID эмитента iss: issuerKeyPair.issuerDid, // DID субъекта (держателя) sub: subjectId, // Время выпуска удостоверения (iat) и время истечения срока его действия (exp) iat: now, exp: now + oneYear, // Модель данных верифицируемого удостоверения vc: { "@context": [ "https://www.w3.org/2018/credentials/v1", "https://europa.eu/eudi/pid/v1", ], type: ["VerifiableCredential", "eu.europa.ec.eudi.pid.1"], issuer: issuerKeyPair.issuerDid, issuanceDate: new Date(now * 1000).toISOString(), credentialSubject: { id: subjectId, ...claims, }, }, }; // Подписываем полезную нагрузку закрытым ключом эмитента return await new SignJWT(vcPayload) .setProtectedHeader({ alg: issuerKeyPair.algorithm, kid: issuerKeyPair.keyId, typ: "JWT", }) .sign(issuerKeyPair.privateKey); }
Эта функция конструирует полезную нагрузку JWT в соответствии с моделью данных W3C Verifiable Credentials и подписывает ее закрытым ключом эмитента, создавая безопасное и верифицируемое удостоверение.
Наше приложение Next.js структурировано таким образом, чтобы разделить обязанности между фронтендом и бэкендом, хотя они и являются частью одного проекта. Это достигается за счет использования App Router как для страниц UI, так и для API-эндпоинтов.
Фронтенд (src/app/issue/page.tsx
): Один компонент страницы
React, который определяет UI для маршрута /issue
. Он
обрабатывает ввод пользователя и взаимодействует с нашим бэкенд-API.
Бэкенд API Routes (src/app/api/...
):
.well-known/.../route.ts
): Эти маршруты предоставляют публичные
эндпоинты метаданных, которые позволяют кошелькам и другим клиентам обнаруживать
возможности эмитента и его открытые ключи.issue/.../route.ts
): Эти эндпоинты реализуют основную логику
OpenID4VCI, включая создание предложений удостоверений, выпуск токенов и подписание
окончательного удостоверения.schemas/pid/route.ts
): Этот маршрут предоставляет JSON-схему для
удостоверения, определяя его структуру.Библиотека (src/lib/
): Этот каталог содержит переиспользуемую логику, общую для
всего бэкенда.
database.ts
: Управляет всеми взаимодействиями с базой данных, абстрагируя
SQL-запросы.crypto.ts
: Обрабатывает все криптографические операции, такие как генерация ключей
и подписание JWT.Такое четкое разделение делает приложение модульным и более простым в обслуживании.
Примечание: Функция generateIssuerDid()
должна возвращать действительный did:web
,
соответствующий вашему домену эмитента. При развертывании файл .well-known/did.json
должен обслуживаться по протоколу HTTPS на этом домене, чтобы верификаторы могли проверять
удостоверения.
Наш фронтенд представляет собой одну страницу на React, которая предоставляет простую форму для запроса нового цифрового удостоверения пользователями. Его обязанности:
Основная логика обрабатывается в функции handleSubmit
, которая срабатывает, когда
пользователь отправляет форму.
// src/app/issue/page.tsx const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setLoading(true); setError(null); setCredentialOffer(null); try { // 1. Проверка обязательных полей if (!userData.given_name || !userData.family_name || !userData.birth_date) { throw new Error("Please fill in all required fields"); } // 2. Запрос предложения удостоверения с бэкенда const response = await fetch("/api/issue/authorize", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ user_data: userData, }), }); if (!response.ok) { const errorData = await response.json(); throw new Error( errorData.error_description || "Failed to create credential offer", ); } // 3. Установка предложения удостоверения в состояние для отображения QR-кода const result = await response.json(); setCredentialOffer(result); } catch (err) { const errorMessage = (err as Error).message || "Unknown error occurred"; setError(errorMessage); } finally { setLoading(false); } };
Эта функция выполняет три ключевых действия:
POST
-запрос на наш эндпоинт /api/issue/authorize
с данными
пользователя.Остальная часть файла содержит стандартный код React для рендеринга формы и отображения QR-кода. Вы можете просмотреть полный файл в репозитории проекта.
Прежде чем мы создадим бэкенд-API, нам нужно настроить наше окружение и эндпоинты
обнаружения. Эти файлы .well-known
имеют решающее значение для того, чтобы кошельки
могли найти нашего эмитента и понять, как с ним взаимодействовать.
Создайте файл с именем .env.local
в корне вашего проекта и добавьте следующую строку.
Этот URL должен быть общедоступным, чтобы мобильный кошелек мог его достичь. Для локальной
разработки вы можете использовать сервис туннелирования, такой как
ngrok, чтобы открыть доступ к вашему
localhost
.
NEXT_PUBLIC_BASE_URL=http://localhost:3000
Кошельки обнаруживают возможности эмитента, запрашивая стандартные URL-адреса
.well-known
. Нам нужно создать три таких эндпоинта.
1. Метаданные эмитента (/.well-known/openid-credential-issuer
)
Это основной файл обнаружения для OpenID4VCI. Он сообщает кошельку все, что ему нужно знать об эмитенте, включая его эндпоинты, типы предлагаемых удостоверений и поддерживаемые криптографические алгоритмы.
Создайте файл src/app/.well-known/openid-credential-issuer/route.ts
:
// src/app/.well-known/openid-credential-issuer/route.ts import { NextResponse } from "next/server"; export async function GET() { const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || "http://localhost:3000"; const issuerMetadata = { // Уникальный идентификатор эмитента. issuer: baseUrl, // URL сервера авторизации. Для простоты наш эмитент является своим собственным сервером авторизации. authorization_servers: [baseUrl], // URL эмитента удостоверений. credential_issuer: baseUrl, // Эндпоинт, на который кошелек отправит POST-запрос для получения фактического удостоверения. credential_endpoint: `${baseUrl}/api/issue/credential`, // Эндпоинт, где кошелек обменивает код авторизации на токен доступа. token_endpoint: `${baseUrl}/api/issue/token`, // Эндпоинт для потока авторизации (не используется в нашем потоке с предварительной авторизацией, но рекомендуется его включить). authorization_endpoint: `${baseUrl}/api/issue/authorize`, // Указывает на поддержку потока с предварительно авторизованным кодом без требования аутентификации клиента. pre_authorized_grant_anonymous_access_supported: true, // Человекочитаемая информация об эмитенте. display: [ { name: "Corbado Credentials Issuer", locale: "en-US", }, ], // Список типов удостоверений, которые может выпускать этот эмитент. credential_configurations_supported: { "eu.europa.ec.eudi.pid.1": { // Формат удостоверения (например, jwt_vc, mso_mdoc). format: "jwt_vc", // Конкретный тип документа, соответствующий стандартам ISO mDoc. doctype: "eu.europa.ec.eudi.pid.1", // Область OAuth 2.0, связанная с этим типом удостоверения. scope: "eu.europa.ec.eudi.pid.1", // Методы, которые кошелек может использовать для подтверждения владения своим ключом. cryptographic_binding_methods_supported: ["jwk"], // Алгоритмы подписи, поддерживаемые эмитентом для этого удостоверения. credential_signing_alg_values_supported: ["ES256"], // Типы подтверждения владения, которые может использовать кошелек. proof_types_supported: { jwt: { proof_signing_alg_values_supported: ["ES256", "ES384", "ES512"], }, }, // Свойства отображения для удостоверения. display: [ { name: "Corbado Credential Issuer", locale: "en-US", logo: { uri: `${baseUrl}/logo.png`, alt_text: "EU Digital Identity", }, background_color: "#003399", text_color: "#FFFFFF", }, ], // Список заявлений (атрибутов) в удостоверении. claims: { "eu.europa.ec.eudi.pid.1": { given_name: { mandatory: true, display: [{ name: "Given Name", locale: "en-US" }], }, family_name: { mandatory: true, display: [{ name: "Family Name", locale: "en-US" }], }, birth_date: { mandatory: true, display: [{ name: "Date of Birth", locale: "en-US" }], }, }, }, }, }, // Методы аутентификации, поддерживаемые эндпоинтом токенов. 'none' означает публичный клиент. token_endpoint_auth_methods_supported: ["none"], // Поддерживаемые методы вызова PKCE. code_challenge_methods_supported: ["S256"], // Типы предоставления прав OAuth 2.0, поддерживаемые эмитентом. grant_types_supported: [ "authorization_code", "urn:ietf:params:oauth:grant-type:pre-authorized_code", ], }; return NextResponse.json(issuerMetadata, { headers: { "Content-Type": "application/json", "Cache-Control": "no-cache, no-store, must-revalidate", Pragma: "no-cache", Expires: "0", }, }); }
2. Конфигурация OpenID (/.well-known/openid-configuration
)
Это стандартный документ обнаружения OIDC, который предоставляет более широкий набор деталей конфигурации.
Создайте файл src/app/.well-known/openid-configuration/route.ts
:
// src/app/.well-known/openid-configuration/route.ts import { NextResponse } from "next/server"; export async function GET() { const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || "http://localhost:3000"; const openidConfiguration = { // Уникальный идентификатор эмитента. credential_issuer: baseUrl, // Эндпоинт, на который кошелек отправит POST-запрос для получения фактического удостоверения. credential_endpoint: `${baseUrl}/api/issue/credential`, // Эндпоинт для потока авторизации. authorization_endpoint: `${baseUrl}/api/issue/authorize`, // Эндпоинт, где кошелек обменивает код авторизации на токен доступа. token_endpoint: `${baseUrl}/api/issue/token`, // Список типов удостоверений, которые может выпускать этот эмитент. credential_configurations_supported: { "eu.europa.ec.eudi.pid.1": { format: "jwt_vc", scope: "eu.europa.ec.eudi.pid.1", cryptographic_binding_methods_supported: ["jwk"], credential_signing_alg_values_supported: ["ES256", "ES384", "ES512"], proof_types_supported: { jwt: { proof_signing_alg_values_supported: ["ES256", "ES384", "ES512"], }, }, }, }, // Типы предоставления прав OAuth 2.0, поддерживаемые эмитентом. grant_types_supported: [ "authorization_code", "urn:ietf:params:oauth:grant-type:pre-authorized_code", ], // Указывает на поддержку потока с предварительно авторизованным кодом. pre_authorized_grant_anonymous_access_supported: true, // Поддерживаемые методы вызова PKCE. code_challenge_methods_supported: ["S256"], // Методы аутентификации, поддерживаемые эндпоинтом токенов. token_endpoint_auth_methods_supported: ["none"], // Области OAuth 2.0, поддерживаемые эмитентом. scopes_supported: ["eu.europa.ec.eudi.pid.1"], }; return NextResponse.json(openidConfiguration, { headers: { "Content-Type": "application/json", "Cache-Control": "no-cache, no-store, must-revalidate", Pragma: "no-cache", Expires: "0", }, }); }
3. DID-документ (/.well-known/did.json
)
Этот файл публикует открытый ключ эмитента с использованием метода did:web
, позволяя
любому проверить подпись выданных им удостоверений.
Создайте файл src/app/.well-known/did.json/route.ts
:
// src/app/.well-known/did.json/route.ts import { NextResponse } from "next/server"; import { getActiveIssuerKey } from "../../../lib/database"; import { generateIssuerDid } from "../../../lib/crypto"; export async function GET() { const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || "http://localhost:3000"; const issuerKey = await getActiveIssuerKey(); if (!issuerKey) { return NextResponse.json( { error: "No active issuer key found" }, { status: 404 }, ); } const publicKeyJWK = JSON.parse(issuerKey.public_key); const didId = generateIssuerDid(); const didDocument = { // Контекст определяет словарь, используемый в документе. "@context": [ "https://www.w3.org/ns/did/v1", "https://w3id.org/security/suites/jws-2020/v1", ], // DID URI, который является уникальным идентификатором эмитента. id: didId, // Контроллер DID, то есть сущность, которая контролирует DID. Здесь это сам эмитент. controller: didId, // Список открытых ключей, которые можно использовать для проверки подписей от эмитента. verificationMethod: [ { // Уникальный идентификатор ключа в рамках DID. id: `${didId}#${issuerKey.key_id}`, // Тип ключа. type: "JsonWebKey2020", // DID контроллера ключа. controller: didId, // Открытый ключ в формате JWK. publicKeyJwk: publicKeyJWK, }, ], // Указывает, какие ключи можно использовать для аутентификации (доказательства контроля над DID). authentication: [`${didId}#${issuerKey.key_id}`], // Указывает, какие ключи можно использовать для создания верифицируемых удостоверений. assertionMethod: [`${didId}#${issuerKey.key_id}`], // Список сервисов, предоставляемых субъектом DID, таких как эндпоинт эмитента. service: [ { id: `${didId}#openid-credential-issuer`, type: "OpenIDCredentialIssuer", serviceEndpoint: `${baseUrl}/.well-known/openid-credential-issuer`, }, ], }; return NextResponse.json(didDocument, { headers: { "Content-Type": "application/did+json", "Cache-Control": "no-cache, no-store, must-revalidate", Pragma: "no-cache", Expires: "0", }, }); }
Почему нет кеширования? Вы заметите, что все три этих эндпоинта возвращают
заголовки, которые агрессивно предотвращают кеширование (Cache-Control: no-cache
,
Pragma: no-cache
, Expires: 0
). Это критически важная практика безопасности для
документов обнаружения. Конфигурации эмитента могут меняться — например, может быть
произведена ротация криптографического ключа. Если бы кошелек или клиент закешировал
старую версию файла did.json
или openid-credential-issuer
, он не смог бы проверить
новые удостоверения или взаимодействовать с обновленными эндпоинтами. Заставляя клиентов
каждый раз запрашивать свежую копию, мы гарантируем, что у них всегда будет самая
актуальная информация.
Последний элемент нашей публичной инфраструктуры — это эндпоинт схемы удостоверения. Этот маршрут предоставляет JSON-схему, которая формально определяет структуру, типы данных и ограничения удостоверения PID, которое мы выпускаем. Кошельки и верификаторы могут использовать эту схему для проверки содержимого удостоверения.
Создайте файл src/app/api/schemas/pid/route.ts
со следующим содержимым:
// src/app/api/schemas/pid/route.ts import { NextResponse } from "next/server"; export async function GET() { const schema = { $schema: "https://json-schema.org/draft/2020-12/schema", $id: "https://example.com/schemas/pid", // Замените на ваш реальный домен title: "PID Credential", description: "A schema for a Verifiable Credential representing a Personal Identification Document (PID).", type: "object", properties: { credentialSubject: { type: "object", properties: { given_name: { type: "string" }, family_name: { type: "string" }, birth_date: { type: "string", format: "date" }, // ... другие свойства субъекта удостоверения }, required: ["given_name", "family_name", "birth_date"], }, // ... другие свойства верхнего уровня верифицируемого удостоверения }, }; return NextResponse.json(schema, { headers: { "Content-Type": "application/schema+json", "Access-Control-Allow-Origin": "*", // Разрешить кросс-доменные запросы }, }); }
Примечание: JSON-схема для удостоверения PID может быть довольно большой и детализированной. Для краткости полная схема была усечена. Вы можете найти полный файл в репозитории проекта.
Имея готовый фронтенд, нам теперь нужна серверная логика для обработки потока OpenID4VCI.
Мы начнем с первого эндпоинта, который вызывает фронтенд: /api/issue/authorize
.
/api/issue/authorize
: Создание предложения удостоверения#Этот эндпоинт отвечает за получение данных пользователя, генерацию безопасного
одноразового кода и конструирование credential_offer
, который сможет понять кошелек
пользователя.
Вот основная логика:
// src/app/api/issue/authorize/route.ts import { NextRequest, NextResponse } from "next/server"; import { v4 as uuidv4 } from "uuid"; import { createAuthorizationCode } from "@/lib/database"; export async function POST(request: NextRequest) { try { const body = await request.json(); const { user_data } = body; // 1. Проверка данных пользователя if ( !user_data || !user_data.given_name || !user_data.family_name || !user_data.birth_date ) { return NextResponse.json({ error: "missing_user_data" }, { status: 400 }); } // 2. Генерация предварительно авторизованного кода и PIN-кода const code = uuidv4(); const expiresAt = new Date(Date.now() + 10 * 60 * 1000); // 10 минут const txCode = Math.floor(1000 + Math.random() * 9000).toString(); // 4-значный PIN // 3. Хранение кода и данных пользователя await createAuthorizationCode(uuidv4(), code, expiresAt); // Примечание: здесь используется хранилище в памяти только для демонстрационных целей. // В продакшене данные следует безопасно хранить в базе данных с правильным сроком действия. if (!(global as any).userDataStore) (global as any).userDataStore = new Map(); (global as any).userDataStore.set(code, user_data); if (!(global as any).txCodeStore) (global as any).txCodeStore = new Map(); (global as any).txCodeStore.set(code, txCode); // 4. Создание объекта предложения удостоверения const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || "http://localhost:3000"; const credentialOffer = { // Идентификатор эмитента, который является его базовым URL. credential_issuer: baseUrl, // Массив типов удостоверений, которые предлагает эмитент. credential_configuration_ids: ["eu.europa.ec.eudi.pid.1"], // Указывает типы предоставления прав, которые может использовать кошелек. grants: { // Мы используем поток с предварительно авторизованным кодом. "urn:ietf:params:oauth:grant-type:pre-authorized_code": { // Одноразовый код, который кошелек обменяет на токен. "pre-authorized_code": code, // Указывает, что пользователь должен ввести PIN-код (tx_code) для использования кода. user_pin_required: true, }, }, }; // 5. Создание полного URI предложения удостоверения (глубокая ссылка для кошельков) const credentialOfferUri = `openid-credential-offer://?credential_offer=${encodeURIComponent( JSON.stringify(credentialOffer), )}`; // Окончательный ответ фронтенду. return NextResponse.json({ // Глубокая ссылка для QR-кода. credential_offer_uri: credentialOfferUri, // Необработанный предварительно авторизованный код для отображения или ручного ввода. pre_authorized_code: code, // 4-значный PIN, который пользователь должен ввести в своем кошельке. tx_code: txCode, }); } catch (error) { console.error("Authorization error:", error); return NextResponse.json({ error: "server_error" }, { status: 500 }); } }
Ключевые шаги в этом эндпоинте:
pre-authorized_code
(UUID) и 4-значный
tx_code
(PIN) для дополнительного уровня безопасности.pre-authorized_code
сохраняется в базе данных с коротким
сроком действия. Данные пользователя и PIN-код хранятся в памяти, связанные с кодом.credential_offer
в соответствии со
спецификацией OpenID4VCI. Этот объект сообщает кошельку, где находится эмитент, какие
удостоверения он предлагает и какой код нужен для их получения.openid-credential-offer://...
) и возвращает его фронтенду вместе с tx_code
для
отображения пользователю./api/issue/token
: Обмен кода на токен#Как только пользователь сканирует QR-код и вводит свой PIN-код, кошелек делает
POST
-запрос на этот эндпоинт. Его задача — проверить pre-authorized_code
и user_pin
(PIN) и, если они действительны, выпустить кратковременный
токен доступа.
Создайте файл src/app/api/issue/token/route.ts
со следующим содержимым:
// src/app/api/issue/token/route.ts import { NextRequest, NextResponse } from "next/server"; import { v4 as uuidv4 } from "uuid"; import { getAuthorizationCode, markAuthorizationCodeAsUsed, createIssuanceSession, } from "@/lib/database"; export async function POST(request: NextRequest) { try { const formData = await request.formData(); const grant_type = formData.get("grant_type") as string; const code = formData.get("pre-authorized_code") as string; const user_pin = formData.get("user_pin") as string; // 1. Проверка типа предоставления прав if (grant_type !== "urn:ietf:params:oauth:grant-type:pre-authorized_code") { return NextResponse.json( { error: "unsupported_grant_type" }, { status: 400 }, ); } // 2. Проверка предварительно авторизованного кода const authCode = await getAuthorizationCode(code); if (!authCode) { return NextResponse.json( { error: "invalid_grant", error_description: "Invalid or expired code", }, { status: 400 }, ); } // 3. Проверка PIN-кода (tx_code) const expectedTxCode = (global as any).txCodeStore?.get(code); if (expectedTxCode !== user_pin) { return NextResponse.json( { error: "invalid_grant", error_description: "Invalid PIN" }, { status: 400 }, ); } // 4. Генерация токена доступа и c_nonce const accessToken = uuidv4(); const cNonce = uuidv4(); const cNonceExpiresAt = new Date(Date.now() + 5 * 60 * 1000); // 5 минут // 5. Создание новой сессии выпуска const userData = (global as any).userDataStore?.get(code); await createIssuanceSession( uuidv4(), authCode.id, accessToken, cNonce, cNonceExpiresAt, userData, ); // 6. Пометка кода как использованного и очистка временных данных await markAuthorizationCodeAsUsed(code); (global as any).txCodeStore?.delete(code); (global as any).userDataStore?.delete(code); // 7. Возврат ответа с токеном доступа return NextResponse.json({ access_token: accessToken, token_type: "Bearer", expires_in: 3600, // 1 час c_nonce: cNonce, c_nonce_expires_in: 300, // 5 минут }); } catch (error) { console.error("Token endpoint error:", error); return NextResponse.json({ error: "server_error" }, { status: 500 }); } }
Ключевые шаги в этом эндпоинте:
pre-authorized_code
.pre-authorized_code
существует в базе данных, не
истек и не был использован ранее.user_pin
из кошелька с tx_code
, который мы
сохранили ранее, чтобы убедиться, что пользователь авторизовал транзакцию.access_token
и c_nonce
(credential
nonce) — одноразовое значение для предотвращения атак повторного воспроизведения на
эндпоинте удостоверений.issuance_sessions
в базе данных,
связывая токен доступа с данными пользователя.pre-authorized_code
как использованный.access_token
и c_nonce
кошельку./api/issue/credential
: Выпуск подписанного удостоверения#Это последний и самый важный эндпоинт. Кошелек использует токен доступа, полученный с
эндпоинта /token
, чтобы сделать аутентифицированный POST
-запрос на этот маршрут.
Задача этого эндпоинта — выполнить окончательную проверку, создать криптографически
подписанное удостоверение и вернуть его кошельку.
Создайте файл src/app/api/issue/credential/route.ts
со следующим содержимым:
// src/app/api/issue/credential/route.ts import { NextRequest, NextResponse } from "next/server"; import { v4 as uuidv4 } from "uuid"; import { getIssuanceSessionByToken, updateIssuanceSession, createIssuedCredential, getActiveIssuerKey, } from "@/lib/database"; import { createJWTVerifiableCredential, importIssuerKeyPair, generateIssuerDid, } from "@/lib/crypto"; export async function POST(request: NextRequest) { try { // 1. Проверка Bearer токена const authHeader = request.headers.get("authorization"); const accessToken = authHeader?.substring(7); const session = await getIssuanceSessionByToken(accessToken); if (!session) { return NextResponse.json({ error: "invalid_token" }, { status: 401 }); } // 2. Получение данных пользователя из сессии const userData = session.user_data; if (!userData) { return NextResponse.json({ error: "missing_user_data" }, { status: 400 }); } // 3. Получение активного ключа эмитента const issuerKey = await getActiveIssuerKey(); if (!issuerKey) { // В реальном приложении у вас была бы более надежная система управления ключами. // Для этого демо мы можем сгенерировать ключ на лету, если он не существует. // Эта часть опущена для краткости, но есть в репозитории. return NextResponse.json( { error: "server_error", error_description: "Failed to get issuer key", }, { status: 500 }, ); } // 4. Создание JWT-VC const issuerDid = generateIssuerDid(); const keyPair = await importIssuerKeyPair( issuerKey.key_id, issuerKey.public_key, issuerKey.private_key, issuerDid, ); const subjectId = `did:example:${uuidv4()}`; const credentialData = await createJWTVerifiableCredential( userData, keyPair, subjectId, process.env.NEXT_PUBLIC_BASE_URL || "http://localhost:3000", ); // 5. Сохранение выпущенного удостоверения в базе данных await createIssuedCredential(/* ... credential details ... */); await updateIssuanceSession(session.id, "credential_issued"); // 6. Возврат подписанного удостоверения return NextResponse.json({ format: "jwt_vc", credential: credentialData, c_nonce: uuidv4(), // Новый nonce для последующих запросов c_nonce_expires_in: 300, }); } catch (error) { console.error("Credential endpoint error:", error); return NextResponse.json({ error: "server_error" }, { status: 500 }); } }
Ключевые шаги в этом эндпоинте:
Bearer
токена в заголовке
Authorization
и использует его для поиска активной сессии выпуска.createJWTVerifiableCredential
из
src/lib/crypto.ts
для конструирования и подписи JWT-VC.Теперь у вас есть полная, сквозная реализация эмитента цифровых удостоверений. Вот как запустить его локально и что нужно учесть, чтобы превратить его из прототипа в готовое к продакшену приложение.
Склонируйте репозиторий:
git clone https://github.com/corbado/digital-credentials-example.git cd digital-credentials-example
Установите зависимости:
npm install
Запустите базу данных: Убедитесь, что Docker запущен, затем запустите контейнер MySQL:
docker-compose up -d
Настройте окружение и запустите туннель: Это самый важный шаг для локального
тестирования. Поскольку вашему мобильному кошельку необходимо подключаться к вашей
машине разработки через интернет, вы должны предоставить вашему локальному серверу
публичный HTTPS URL. Мы будем использовать для этого ngrok
.
a. Запустите ngrok:
ngrok http 3000
b. Скопируйте HTTPS URL из вывода
ngrok (например,
https://random-string.ngrok.io
). c. Создайте файл .env.local
и установите URL:
NEXT_PUBLIC_BASE_URL=https://<your-ngrok-url>
Запустите приложение:
npm run dev
Откройте браузер по адресу http://localhost:3000/issue
. Теперь вы можете заполнить
форму, и сгенерированный QR-код будет правильно указывать на ваш публичный URL ngrok,
позволяя вашему мобильному кошельку подключиться и получить удостоверение.
ngrok
#Протоколы цифровых удостоверений создаются с приоритетом на безопасность. По этой причине
кошельки почти всегда отказываются подключаться к эмитенту через небезопасное соединение
(http://
). Весь процесс основан на безопасном соединении HTTPS, которое
обеспечивается SSL-сертификатом.
Сервис туннелирования, такой как ngrok
, решает обе проблемы, создавая безопасный,
общедоступный HTTPS URL (с действительным SSL-сертификатом), который перенаправляет весь
трафик на ваш локальный сервер разработки. Кошельки требуют HTTPS и отказываются
подключаться к небезопасным (http://
) эндпоинтам. Это незаменимый инструмент для
тестирования любого веб-сервиса, которому необходимо взаимодействовать с мобильными
устройствами или внешними веб-хуками.
Этот пример намеренно сфокусирован на основном процессе выпуска, чтобы сделать его легким для понимания. Следующие темы выходят за рамки:
revoked
для будущего использования, логика
отзыва здесь не предоставлена.pre-authorized_code
. Полная реализация потока authorization_code
потребовала бы
экрана согласия пользователя и более сложной логики OAuth 2.0.Вот и всё! С помощью нескольких страниц кода мы получили полный, сквозной эмитент цифровых удостоверений, который:
pre-authorized_code
.Хотя это руководство предоставляет прочную основу, готовый к продакшену эмитент потребует дополнительных функций, таких как надежное управление ключами, постоянное хранилище вместо хранения в памяти, отзыв удостоверений и всестороннее усиление безопасности. Совместимость с кошельками также может различаться; для тестирования рекомендуется Sphereon Wallet, но другие кошельки могут не поддерживать поток с предварительной авторизацией в том виде, в котором он здесь реализован. Однако основные строительные блоки и схема взаимодействия останутся прежними. Следуя этим шаблонам, вы можете создать безопасный и совместимый эмитент для любого типа цифровых удостоверений.
Вот некоторые из ключевых ресурсов, спецификаций и инструментов, использованных или упомянутых в этом руководстве:
Репозиторий проекта:
Ключевые спецификации:
did:web
: Метод DID,
используемый для открытого ключа нашего эмитента.Инструменты:
Библиотеки:
Related Articles
Table of Contents