Get your free and exclusive 80-page Banking Passkey Report
Back to Overview

Как создать эмитент цифровых удостоверений (руководство для разработчиков)

Узнайте, как создать эмитент верифицируемых удостоверений W3C с использованием протокола OpenID4VCI. В этом пошаговом руководстве мы покажем, как создать приложение на Next.js, которое выпускает криптографически подписанные удостоверения, совместимые с ци

Amine

Created: August 20, 2025

Updated: August 21, 2025

Blog-Post-Header-Image

See the original blog version in English here.

DigitalCredentialsDemo Icon

Want to experience digital credentials in action?

Try Digital Credentials

1. Введение#

Цифровые удостоверения — это мощный способ подтверждения личности и заявлений безопасным и конфиденциальным образом. Но как пользователи получают эти удостоверения? Именно здесь решающую роль играет эмитент. Эмитент — это доверенная организация, например государственное учреждение, университет или банк, которая отвечает за создание и выдачу пользователям удостоверений с цифровой подписью.

Это руководство представляет собой подробное пошаговое пособие по созданию эмитента цифровых удостоверений. Мы сосредоточимся на протоколе OpenID for Verifiable Credential Issuance (OpenID4VCI) — современном стандарте, который определяет, как пользователи могут получать удостоверения от эмитента и безопасно хранить их в своих цифровых кошельках.

В результате мы получим работающее приложение на Next.js, которое сможет:

  1. Принимать данные пользователя через простую веб-форму.
  2. Генерировать безопасное одноразовое предложение удостоверения (credential offer).
  3. Отображать предложение в виде QR-кода, который пользователь может отсканировать своим мобильным кошельком.
  4. Выпускать криптографически подписанное удостоверение, которое пользователь может сохранить и предъявлять для проверки.

1.1. Разбираемся в терминологии: цифровые и верифицируемые удостоверения#

Прежде чем продолжить, важно прояснить различие между двумя связанными, но разными понятиями:

  • Цифровые удостоверения (общий термин): Это широкая категория, которая охватывает любую цифровую форму удостоверений, сертификатов или аттестаций. Это могут быть простые цифровые сертификаты, базовые цифровые значки или любые удостоверения, хранящиеся в электронном виде, которые могут иметь или не иметь криптографических средств защиты.

  • Верифицируемые удостоверения (VC — стандарт W3C): Это особый тип цифровых удостоверений, который соответствует стандарту W3C Verifiable Credentials Data Model. Верифицируемые удостоверения — это криптографически подписанные, защищенные от подделки и сохраняющие конфиденциальность удостоверения, которые можно проверить независимо. Они включают в себя особые технические требования, такие как:

    • Криптографические подписи для подлинности и целостности
    • Стандартизированная модель данных и форматы
    • Механизмы представления, сохраняющие конфиденциальность
    • Совместимые протоколы верификации

В этом руководстве мы создаем именно эмитент верифицируемых удостоверений, который соответствует стандарту W3C, а не просто любую систему цифровых удостоверений. Протокол OpenID4VCI, который мы используем, специально разработан для выпуска верифицируемых удостоверений, а формат JWT-VC, который мы будем реализовывать, является совместимым со стандартом W3C форматом для верифицируемых удостоверений.

1.2. Как это работает#

Магия цифровых удостоверений заключается в простой, но мощной модели «треугольника доверия», в которой участвуют три ключевых игрока:

  • Эмитент: Доверенная организация (например, государственное учреждение, университет или банк), которая криптографически подписывает и выдает удостоверение пользователю. Именно эту роль мы и создаем в данном руководстве.
  • Держатель: Пользователь, который получает удостоверение и безопасно хранит его в личном цифровом кошельке на своем устройстве.
  • Верификатор: Приложение или сервис, которому необходимо проверить удостоверение пользователя.

Процесс выпуска — это первый шаг в этой экосистеме. Эмитент проверяет информацию пользователя и предоставляет ему удостоверение. Как только держатель получает это удостоверение в свой кошелек, он может предъявить его верификатору, чтобы подтвердить свою личность или заявления, замыкая треугольник.

Вот краткий обзор работы готового приложения:

Шаг 1: Ввод данных пользователя Пользователь заполняет форму со своими личными данными, чтобы запросить новое удостоверение.

Шаг 2: Генерация предложения удостоверения Приложение генерирует безопасное предложение удостоверения, отображаемое в виде QR-кода и предварительно авторизованного кода.

Шаг 3: Взаимодействие с кошельком Пользователь сканирует QR-код совместимым кошельком (например, Sphereon Wallet) и вводит PIN-код для авторизации выпуска.

Шаг 4: Удостоверение выпущено Кошелек получает и сохраняет только что выпущенное цифровое удостоверение, готовое к использованию в будущем.

2. Предварительные требования для создания эмитента#

Прежде чем мы углубимся в код, давайте рассмотрим основные знания и инструменты, которые нам понадобятся. Это руководство предполагает, что у вас есть базовое знакомство с концепциями веб-разработки, но следующие предварительные требования необходимы для создания эмитента удостоверений.

2.1. Выбор протоколов#

Наш эмитент построен на наборе открытых стандартов, которые обеспечивают совместимость между кошельками и сервисами выпуска. В этом руководстве мы сосредоточимся на следующих:

Стандарт / ПротоколОписание
OpenID4VCIOpenID for Verifiable Credential Issuance. Это основной протокол, который мы будем использовать. Он определяет стандартный процесс, с помощью которого пользователь (через свой кошелек) может запросить и получить удостоверение от эмитента.
JWT-VCВерифицируемые удостоверения на основе JWT. Формат удостоверения, которое мы будем выпускать. Это стандарт W3C, который кодирует верифицируемые удостоверения как JSON Web Tokens (JWT), делая их компактными и удобными для использования в вебе.
ISO mDocISO/IEC 18013-5. Международный стандарт для мобильных водительских удостоверений (mDL). Хотя мы выпускаем JWT-VC, заявления (claims) внутри него структурированы так, чтобы быть совместимыми с моделью данных mDoc (например, eu.europa.ec.eudi.pid.1).
OAuth 2.0Базовая платформа авторизации, используемая OpenID4VCI. Мы реализуем поток pre-authorized_code, который является специфическим типом предоставления прав, разработанным для безопасного и удобного выпуска удостоверений.

2.1.1. Потоки авторизации: Pre-Authorized и Authorization Code#

OpenID4VCI поддерживает два основных потока авторизации для выпуска удостоверений:

  1. Поток с предварительно авторизованным кодом (Pre-Authorized Code Flow): В этом потоке эмитент генерирует кратковременный, одноразовый код (pre-authorized_code), который сразу же доступен пользователю. Кошелек пользователя может затем обменять этот код непосредственно на удостоверение. Этот поток идеально подходит для сценариев, когда пользователь уже аутентифицирован и находится на веб-сайте эмитента, поскольку он обеспечивает бесшовный, мгновенный выпуск без перенаправлений.

  2. Поток с кодом авторизации (Authorization Code Flow): Это стандартный поток OAuth 2.0, при котором пользователь перенаправляется на сервер авторизации для предоставления согласия. После одобрения сервер отправляет authorization_code обратно на зарегистрированный redirect_uri. Этот поток больше подходит для сторонних приложений, которые инициируют процесс выпуска от имени пользователя.

В этом руководстве мы будем использовать поток pre-authorized_code. Мы выбрали этот подход, потому что он проще и обеспечивает более прямой пользовательский опыт для нашего конкретного случая: пользователь напрямую запрашивает удостоверение с собственного веб-сайта эмитента. Это устраняет необходимость в сложных перенаправлениях и регистрации клиента, делая основную логику выпуска проще для понимания и реализации.

Эта комбинация стандартов позволяет нам создать эмитент, совместимый с широким спектром цифровых кошельков и обеспечивающий безопасный, стандартизированный процесс для пользователя.

2.2. Выбор технологического стека#

Для создания нашего эмитента мы будем использовать тот же надежный и современный технологический стек, который мы использовали для верификатора, обеспечивая последовательный и высококачественный опыт для разработчиков.

2.2.1. Язык: TypeScript#

Мы будем использовать TypeScript как для фронтенда, так и для бэкенда. Его статическая типизация неоценима в критически важном для безопасности приложении, таком как эмитент, поскольку она помогает предотвратить распространенные ошибки и улучшает общее качество и поддерживаемость кода.

2.2.2. Фреймворк: Next.js#

Next.js — наш выбор фреймворка, потому что он обеспечивает бесшовный, интегрированный опыт для создания full-stack приложений.

  • Для фронтенда: Мы будем использовать Next.js с React для создания пользовательского интерфейса, где пользователи могут вводить свои данные для запроса удостоверения.
  • Для бэкенда: Мы воспользуемся Next.js API Routes для создания серверных эндпоинтов, которые обрабатывают поток OpenID4VCI, от генерации предложений удостоверений до выпуска окончательного подписанного удостоверения.

2.2.3. Ключевые библиотеки#

Наша реализация будет опираться на несколько ключевых библиотек для решения конкретных задач:

  • next, react и react-dom: Основные библиотеки для нашего приложения Next.js.
  • mysql2: Клиент MySQL для Node.js, используемый для хранения кодов авторизации и данных сессий.
  • uuid: Библиотека для генерации уникальных идентификаторов, которую мы будем использовать для создания значений pre-authorized_code.
  • jose: Надежная библиотека для работы с JSON Web Signatures (JWS), которую мы будем использовать для криптографической подписи выпускаемых нами удостоверений.

2.3. Установите тестовый кошелек#

Для тестирования вашего эмитента вам понадобится мобильный кошелек, поддерживающий протокол OpenID4VCI. Для этого руководства мы рекомендуем Sphereon Wallet, который доступен как для Android, так и для iOS.

Как установить Sphereon Wallet:

  1. Загрузите кошелек из Google Play Store или Apple App Store.
  2. Установите приложение на свое мобильное устройство.
  3. После установки кошелек готов к приему предложений удостоверений путем сканирования QR-кода.

2.4. Знания в области криптографии#

Выпуск удостоверения — это критически важная для безопасности операция, которая опирается на фундаментальные криптографические концепции для обеспечения доверия и подлинности.

2.4.1. Цифровые подписи#

По своей сути, верифицируемое удостоверение — это набор заявлений, который был подписан цифровой подписью эмитента. Эта подпись предоставляет две гарантии:

  • Подлинность: Она доказывает, что удостоверение было создано легитимным эмитентом.
  • Целостность: Она доказывает, что удостоверение не было изменено с момента его выпуска.

2.4.2. Криптография с открытым/закрытым ключом#

Цифровые подписи создаются с использованием криптографии с открытым и закрытым ключами. Вот как это работает:

  1. У эмитента есть пара ключей: закрытый ключ, который хранится в секрете и безопасности, и соответствующий открытый ключ, который делается общедоступным.
  2. Подписание: Когда эмитент создает удостоверение, он использует свой закрытый ключ для генерации уникальной цифровой подписи для данных удостоверения.
  3. Верификация: Верификатор может позже использовать открытый ключ эмитента для проверки подписи. Если проверка проходит успешно, верификатор знает, что удостоверение подлинное и не было изменено.

В нашей реализации мы сгенерируем пару ключей на эллиптических кривых (EC) и будем использовать алгоритм ES256 для подписи JWT-VC. Открытый ключ встроен в DID эмитента (did:web), что позволяет любому верификатору обнаружить его и проверить подпись удостоверения. Примечание: Заявление aud (audience) намеренно опущено в наших JWT, поскольку удостоверение предназначено для общего использования и не привязано к конкретному кошельку. Если вы хотите ограничить использование определенной аудиторией, включите заявление aud и установите его соответствующим образом.

3. Обзор архитектуры#

Наше приложение-эмитент построено как full-stack проект на Next.js с четким разделением между логикой фронтенда и бэкенда. Эта архитектура позволяет нам создать бесшовный пользовательский опыт, обрабатывая все критически важные для безопасности операции на сервере. Важно: Включенные в SQL таблицы verification_sessions и verified_credentials не требуются для этого эмитента, но добавлены для полноты картины.

  • Фронтенд (src/app/issue/page.tsx): Одна страница на React, которая позволяет пользователям вводить свои данные для запроса удостоверения. Она делает API-вызовы на наш бэкенд для инициации процесса выпуска.
  • Бэкенд API Routes (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.

Вот диаграмма, иллюстрирующая процесс выпуска:

4. Создание эмитента#

Теперь, когда у нас есть четкое понимание стандартов, протоколов и архитектуры, мы можем приступить к созданию нашего эмитента.

Следуйте инструкциям или используйте готовый код

Сейчас мы пошагово пройдем через настройку и реализацию кода. Если вы предпочитаете сразу перейти к готовому продукту, вы можете склонировать полный проект из нашего репозитория на GitHub и запустить его локально.

git clone https://github.com/corbado/digital-credentials-example.git

4.1. Настройка проекта#

Сначала мы инициализируем новый проект Next.js, установим необходимые зависимости и запустим нашу базу данных.

4.1.1. Инициализация приложения Next.js#

Откройте терминал, перейдите в каталог, где вы хотите создать свой проект, и выполните следующую команду. Мы используем App Router, TypeScript и Tailwind CSS для этого проекта.

npx create-next-app@latest . --ts --eslint --tailwind --app --src-dir --import-alias "@/*" --use-npm

Эта команда создает новый скелет приложения Next.js в вашем текущем каталоге.

4.1.2. Установка зависимостей#

Далее нам нужно установить библиотеки, которые будут обрабатывать JWT, подключения к базе данных и генерацию UUID.

npm install jose mysql2 uuid @types/uuid

Эта команда устанавливает:

  • jose: для подписи и проверки JSON Web Tokens (JWT).
  • mysql2: клиент MySQL для нашей базы данных.
  • uuid: для генерации уникальных строк вызова (challenge strings).
  • @types/uuid: типы TypeScript для библиотеки uuid.

4.1.3. Запуск базы данных#

Нашему бэкенду требуется база данных 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 в фоновом режиме, готовый к использованию нашим приложением.

4.2. Реализация общих библиотек#

Прежде чем создавать API-эндпоинты, давайте создадим общие библиотеки, которые будут обрабатывать основную бизнес-логику. Такой подход позволяет нам держать наши API-маршруты чистыми и сфокусированными на обработке HTTP-запросов, в то время как сложная работа делегируется этим модулям.

4.2.1. Библиотека базы данных (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, сессиями верификации, кодами авторизации, сессиями выпуска и ключами эмитента.

4.2.2. Криптографическая библиотека (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 и подписывает ее закрытым ключом эмитента, создавая безопасное и верифицируемое удостоверение.

4.2. Архитектурный обзор приложения Next.js#

Наше приложение 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 на этом домене, чтобы верификаторы могли проверять удостоверения.

4.3. Создание фронтенда#

Наш фронтенд представляет собой одну страницу на React, которая предоставляет простую форму для запроса нового цифрового удостоверения пользователями. Его обязанности:

  • Собирать данные пользователя (имя, дата рождения и т. д.).
  • Отправлять эти данные на наш бэкенд для создания предложения удостоверения.
  • Отображать полученный QR-код и PIN-код для сканирования пользователем с помощью своего кошелька.

Основная логика обрабатывается в функции 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); } };

Эта функция выполняет три ключевых действия:

  1. Проверяет данные формы, чтобы убедиться, что все обязательные поля заполнены.
  2. Отправляет POST-запрос на наш эндпоинт /api/issue/authorize с данными пользователя.
  3. Обновляет состояние компонента с предложением удостоверения, полученным с бэкенда, что заставляет UI отобразить QR-код и код транзакции.

Остальная часть файла содержит стандартный код React для рендеринга формы и отображения QR-кода. Вы можете просмотреть полный файл в репозитории проекта.

4.4. Настройка окружения и обнаружения#

Прежде чем мы создадим бэкенд-API, нам нужно настроить наше окружение и эндпоинты обнаружения. Эти файлы .well-known имеют решающее значение для того, чтобы кошельки могли найти нашего эмитента и понять, как с ним взаимодействовать.

4.4.1. Создание файла окружения#

Создайте файл с именем .env.local в корне вашего проекта и добавьте следующую строку. Этот URL должен быть общедоступным, чтобы мобильный кошелек мог его достичь. Для локальной разработки вы можете использовать сервис туннелирования, такой как ngrok, чтобы открыть доступ к вашему localhost.

NEXT_PUBLIC_BASE_URL=http://localhost:3000

4.4.2. Реализация эндпоинтов обнаружения#

Кошельки обнаруживают возможности эмитента, запрашивая стандартные 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, он не смог бы проверить новые удостоверения или взаимодействовать с обновленными эндпоинтами. Заставляя клиентов каждый раз запрашивать свежую копию, мы гарантируем, что у них всегда будет самая актуальная информация.

4.4.3. Реализация эндпоинта схемы удостоверения#

Последний элемент нашей публичной инфраструктуры — это эндпоинт схемы удостоверения. Этот маршрут предоставляет 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 может быть довольно большой и детализированной. Для краткости полная схема была усечена. Вы можете найти полный файл в репозитории проекта.

4.5. Создание бэкенд-эндпоинтов#

Имея готовый фронтенд, нам теперь нужна серверная логика для обработки потока OpenID4VCI. Мы начнем с первого эндпоинта, который вызывает фронтенд: /api/issue/authorize.

4.5.1. /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 }); } }

Ключевые шаги в этом эндпоинте:

  1. Проверка данных: Сначала он убеждается, что необходимые данные пользователя присутствуют.
  2. Генерация кодов: Он создает уникальный pre-authorized_code (UUID) и 4-значный tx_code (PIN) для дополнительного уровня безопасности.
  3. Сохранение данных: pre-authorized_code сохраняется в базе данных с коротким сроком действия. Данные пользователя и PIN-код хранятся в памяти, связанные с кодом.
  4. Построение предложения: Он конструирует объект credential_offer в соответствии со спецификацией OpenID4VCI. Этот объект сообщает кошельку, где находится эмитент, какие удостоверения он предлагает и какой код нужен для их получения.
  5. Возврат URI: Наконец, он создает URI глубокой ссылки (openid-credential-offer://...) и возвращает его фронтенду вместе с tx_code для отображения пользователю.

4.5.2. /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 }); } }

Ключевые шаги в этом эндпоинте:

  1. Проверка типа предоставления прав: Он гарантирует, что кошелек использует правильный тип pre-authorized_code.
  2. Проверка кода: Он проверяет, что pre-authorized_code существует в базе данных, не истек и не был использован ранее.
  3. Проверка PIN-кода: Он сравнивает user_pin из кошелька с tx_code, который мы сохранили ранее, чтобы убедиться, что пользователь авторизовал транзакцию.
  4. Генерация токенов: Он создает безопасный access_token и c_nonce (credential nonce) — одноразовое значение для предотвращения атак повторного воспроизведения на эндпоинте удостоверений.
  5. Создание сессии: Он создает новую запись issuance_sessions в базе данных, связывая токен доступа с данными пользователя.
  6. Пометка кода как использованного: Чтобы предотвратить повторное использование того же предложения, он помечает pre-authorized_code как использованный.
  7. Возврат токена: Он возвращает access_token и c_nonce кошельку.

4.5.3. /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 }); } }

Ключевые шаги в этом эндпоинте:

  1. Проверка токена: Он проверяет наличие действительного Bearer токена в заголовке Authorization и использует его для поиска активной сессии выпуска.
  2. Получение данных пользователя: Он извлекает данные заявлений пользователя, которые были сохранены в сессии при создании токена.
  3. Загрузка ключа эмитента: Он загружает активный ключ подписи эмитента из базы данных. В реальном сценарии это управлялось бы безопасной системой управления ключами.
  4. Создание удостоверения: Он вызывает наш хелпер createJWTVerifiableCredential из src/lib/crypto.ts для конструирования и подписи JWT-VC.
  5. Логирование выпуска: Он сохраняет запись о выпущенном удостоверении в базе данных для целей аудита и отзыва.
  6. Возврат удостоверения: Он возвращает подписанное удостоверение кошельку в JSON-ответе. Кошелек затем несет ответственность за его безопасное хранение.

5. Запуск эмитента и следующие шаги#

Теперь у вас есть полная, сквозная реализация эмитента цифровых удостоверений. Вот как запустить его локально и что нужно учесть, чтобы превратить его из прототипа в готовое к продакшену приложение.

5.1. Как запустить пример#

  1. Склонируйте репозиторий:

    git clone https://github.com/corbado/digital-credentials-example.git cd digital-credentials-example
  2. Установите зависимости:

    npm install
  3. Запустите базу данных: Убедитесь, что Docker запущен, затем запустите контейнер MySQL:

    docker-compose up -d
  4. Настройте окружение и запустите туннель: Это самый важный шаг для локального тестирования. Поскольку вашему мобильному кошельку необходимо подключаться к вашей машине разработки через интернет, вы должны предоставить вашему локальному серверу публичный 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>
  5. Запустите приложение:

    npm run dev

    Откройте браузер по адресу http://localhost:3000/issue. Теперь вы можете заполнить форму, и сгенерированный QR-код будет правильно указывать на ваш публичный URL ngrok, позволяя вашему мобильному кошельку подключиться и получить удостоверение.

5.2. Важность HTTPS и ngrok#

Протоколы цифровых удостоверений создаются с приоритетом на безопасность. По этой причине кошельки почти всегда отказываются подключаться к эмитенту через небезопасное соединение (http://). Весь процесс основан на безопасном соединении HTTPS, которое обеспечивается SSL-сертификатом.

Сервис туннелирования, такой как ngrok, решает обе проблемы, создавая безопасный, общедоступный HTTPS URL (с действительным SSL-сертификатом), который перенаправляет весь трафик на ваш локальный сервер разработки. Кошельки требуют HTTPS и отказываются подключаться к небезопасным (http://) эндпоинтам. Это незаменимый инструмент для тестирования любого веб-сервиса, которому необходимо взаимодействовать с мобильными устройствами или внешними веб-хуками.

5.3. Что выходит за рамки этого руководства#

Этот пример намеренно сфокусирован на основном процессе выпуска, чтобы сделать его легким для понимания. Следующие темы выходят за рамки:

  • Безопасность на уровне продакшена: Этот эмитент предназначен для образовательных целей. Продакшн-система потребовала бы безопасной системы управления ключами (KMS) вместо хранения ключей в базе данных, надежной обработки ошибок, ограничения скорости запросов и всестороннего аудита.
  • Отзыв удостоверений: В этом руководстве не реализован механизм отзыва выпущенных удостоверений. Хотя схема включает флаг revoked для будущего использования, логика отзыва здесь не предоставлена.
  • Поток с кодом авторизации: Мы сосредоточились исключительно на потоке pre-authorized_code. Полная реализация потока authorization_code потребовала бы экрана согласия пользователя и более сложной логики OAuth 2.0.
  • Управление пользователями: Руководство не включает аутентификацию или управление пользователями для самого эмитента. Предполагается, что пользователь уже аутентифицирован и авторизован для получения удостоверения.

6. Заключение#

Вот и всё! С помощью нескольких страниц кода мы получили полный, сквозной эмитент цифровых удостоверений, который:

  1. Предоставляет удобный фронтенд для запроса удостоверений.
  2. Реализует полный поток OpenID4VCI pre-authorized_code.
  3. Предоставляет все необходимые эндпоинты обнаружения для совместимости с кошельками.
  4. Генерирует и подписывает безопасное, соответствующее стандартам JWT-верифицируемое удостоверение.

Хотя это руководство предоставляет прочную основу, готовый к продакшену эмитент потребует дополнительных функций, таких как надежное управление ключами, постоянное хранилище вместо хранения в памяти, отзыв удостоверений и всестороннее усиление безопасности. Совместимость с кошельками также может различаться; для тестирования рекомендуется Sphereon Wallet, но другие кошельки могут не поддерживать поток с предварительной авторизацией в том виде, в котором он здесь реализован. Однако основные строительные блоки и схема взаимодействия останутся прежними. Следуя этим шаблонам, вы можете создать безопасный и совместимый эмитент для любого типа цифровых удостоверений.

7. Ресурсы#

Вот некоторые из ключевых ресурсов, спецификаций и инструментов, использованных или упомянутых в этом руководстве:

Add passkeys to your app in <1 hour with our UI components, SDKs & guides.

Start Free Trial

Share this article


LinkedInTwitterFacebook

Table of Contents