Спецификация JWS (JSON Web Signature) определяет несколько форматов представления подписанных данных. В отличие от компактного (Compact Serialization), формат General JSON Serialization позволяет включать несколько подписей для одного и того же полезного содержимого (payload). Это особенно важно в сценариях, где требуется:
Структура General JWS представляет собой JSON-объект:
{
"payload": "<base64url-encoded payload>",
"signatures": [
{
"protected": "<base64url-encoded header>",
"header": { ... },
"signature": "<base64url-encoded signature>"
},
...
]
}
payload — общий для всех подписей
signatures — массив объектов подписи
Каждая подпись может иметь:
protected — защищённый заголовок (участвует в
подписи)header — незащищённый заголовокsignature — сама подписьКаждая подпись формируется независимо, но применяется к одному и тому же payload.
Библиотека jose предоставляет API для создания и
проверки JWS с множественными подписями через класс
GeneralSign.
npm install jose
import { GeneralSign } from 'jose'
import { generateKeyPair } from 'crypto'
const { privateKey: key1 } = await generateKeyPair('rsa', {
modulusLength: 2048,
})
const { privateKey: key2 } = await generateKeyPair('ec', {
namedCurve: 'P-256',
})
const payload = new TextEncoder().encode('Multi-signature payload')
const jws = await new GeneralSign(payload)
.addSignature(key1)
.setProtectedHeader({ alg: 'RS256' })
.addSignature(key2)
.setProtectedHeader({ alg: 'ES256' })
.sign()
console.log(jws)
GeneralSign(payload) — инициализация с полезной
нагрузкойaddSignature(key) — добавление новой подписиsetProtectedHeader(...) — установка заголовка для
текущей подписи.sign() — генерация финального JWS-объектаВажно: вызов setProtectedHeader применяется
только к последней добавленной подписи.
Каждая подпись может иметь:
.addSignature(key1)
.setProtectedHeader({ alg: 'RS256' })
.setUnprotectedHeader({ kid: 'key1-id' })
Проверка производится с использованием
generalVerify:
import { generalVerify } from 'jose'
const { payload, signatures } = await generalVerify(jws, async (protectedHeader) => {
if (protectedHeader.alg === 'RS256') return publicKey1
if (protectedHeader.alg === 'ES256') return publicKey2
})
Функция-резолвер получает protectedHeader и возвращает
соответствующий публичный ключ. Это позволяет:
alg, kid,
iss и др.generalVerify возвращает:
payload — исходные данныеsignatures — массив успешно проверенных подписейКаждый элемент содержит:
{
protectedHeader,
unprotectedHeader,
signature
}
По умолчанию generalVerify требует, чтобы все
подписи были валидны. Однако можно изменить поведение:
await generalVerify(jws, keyResolver, {
complete: true
})
Это позволяет анализировать каждую подпись отдельно, даже если некоторые из них недействительны.
Поле kid (Key ID) помогает выбирать ключ:
.setProtectedHeader({
alg: 'RS256',
kid: 'key-1'
})
Резолвер:
const keyResolver = async (header) => {
return keyStore[header.kid]
}
В одном JWS можно комбинировать:
Пример:
.addSignature(rsaKey)
.setProtectedHeader({ alg: 'RS256' })
.addSignature(ecKey)
.setProtectedHeader({ alg: 'ES256' })
.addSignature(edKey)
.setProtectedHeader({ alg: 'EdDSA' })
Payload кодируется автоматически в Base64URL. При необходимости можно отключить это:
.setProtectedHeader({
alg: 'HS256',
b64: false,
crit: ['b64']
})
Тогда payload передаётся как есть (не закодированным), что требует соблюдения RFC 7797.
Ключевые аспекты:
alg должна быть строгойheader)kid только вместе с проверкой
источника1. Мульти-подпись документов
2. Федеративные системы
3. Миграция алгоритмов
4. Кросс-платформенная верификация
{
"payload": "SGVsbG8gd29ybGQ",
"signatures": [
{
"protected": "eyJhbGciOiJSUzI1NiJ9",
"signature": "abc123..."
},
{
"protected": "eyJhbGciOiJFUzI1NiJ9",
"signature": "xyz456..."
}
]
}
| Формат | Подписей | Использование |
|---|---|---|
| Compact | 1 | HTTP, Authorization |
| Flattened JSON | 1 | JSON API |
| General JSON | несколько | Multi-signature |
crit для нестандартных параметровPayload может быть исключён из JWS:
const jws = await new GeneralSign(payload)
.addSignature(key)
.setProtectedHeader({ alg: 'RS256' })
.sign({ detached: true })
В этом случае payload передаётся отдельно при верификации.
import { createRemoteJWKSet } from 'jose'
const JWKS = createRemoteJWKSet(new URL('https://example.com/.well-known/jwks.json'))
await generalVerify(jws, JWKS)
Позволяет автоматически выбирать ключи по kid.
Типичные ошибки:
JWSInvalid — некорректная структураJWSSignatureVerificationFailed — подпись невалиднаJOSENotSupported — неподдерживаемый алгоритмРекомендуется обрабатывать ошибки отдельно для каждой подписи.
Такой подход обеспечивает гибкость, масштабируемость и высокий уровень доверия в распределённых системах.