diff --git a/README.md b/README.md index 8851e5c..b0eaa24 100644 --- a/README.md +++ b/README.md @@ -34,11 +34,21 @@ MC4CAQAwBQYDK2VwBCIEIDNxA9U1VSG9zoOvcJ5uB+JUe25UD5m9UwMi6slXzW44 ## Запуск +На Windows перед запуском задайте переменную окружения `LICENSE_PRIVATE_KEY_PEM` (многострочное значение в кавычках или через `.env` и загрузчик по желанию). + ```bash npm start ``` -На Windows перед запуском задайте переменную окружения `LICENSE_PRIVATE_KEY_PEM` (многострочное значение в кавычках или через `.env` и загрузчик по желанию). +## Тесты + +Из корня репозитория: + +```bash +npm test +``` + +Запускает модульные тесты Node (`node --test`, см. `package.json` → `test`). ## API diff --git a/lib/signPayload.mjs b/lib/signPayload.mjs new file mode 100644 index 0000000..c83a76b --- /dev/null +++ b/lib/signPayload.mjs @@ -0,0 +1,18 @@ +import { createPrivateKey, sign } from 'node:crypto'; + +import { canonicalJson } from './canonicalJson.mjs'; + +export function b64url(buf) { + return Buffer.from(buf) + .toString('base64') + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=+$/u, ''); +} + +export function signPayload(payload, privateKeyPem) { + const key = createPrivateKey(privateKeyPem); + const body = canonicalJson(payload); + const sig = sign(null, Buffer.from(body, 'utf8'), key); + return `${b64url(Buffer.from(body, 'utf8'))}.${b64url(sig)}`; +} diff --git a/package.json b/package.json index 894b31d..5c0d05f 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,8 @@ "type": "module", "description": "Сервис выдачи и отзыва лицензий DNDGamePlayer (Ed25519)", "scripts": { - "start": "node src/server.mjs" + "start": "node src/server.mjs", + "test": "node --test test/*.mjs" }, "engines": { "node": ">=20" diff --git a/src/server.mjs b/src/server.mjs index 933488f..7cf68f9 100644 --- a/src/server.mjs +++ b/src/server.mjs @@ -1,10 +1,11 @@ -import { createPrivateKey, randomUUID, sign } from 'node:crypto'; +import { randomUUID } from 'node:crypto'; import fs from 'node:fs'; import http from 'node:http'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { canonicalJson } from '../lib/canonicalJson.mjs'; +import { signPayload } from '../lib/signPayload.mjs'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const root = path.resolve(__dirname, '..'); @@ -20,21 +21,6 @@ function writeData(data) { fs.writeFileSync(p, `${JSON.stringify(data, null, 2)}\n`, 'utf8'); } -function b64url(buf) { - return Buffer.from(buf) - .toString('base64') - .replace(/\+/g, '-') - .replace(/\//g, '_') - .replace(/=+$/u, ''); -} - -function signPayload(payload, privateKeyPem) { - const key = createPrivateKey(privateKeyPem); - const body = canonicalJson(payload); - const sig = sign(null, Buffer.from(body, 'utf8'), key); - return `${b64url(Buffer.from(body, 'utf8'))}.${b64url(sig)}`; -} - function json(res, code, obj) { const body = JSON.stringify(obj); res.writeHead(code, { diff --git a/test/canonicalJson.test.mjs b/test/canonicalJson.test.mjs new file mode 100644 index 0000000..4ae881f --- /dev/null +++ b/test/canonicalJson.test.mjs @@ -0,0 +1,15 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { canonicalJson } from '../lib/canonicalJson.mjs'; + +void test('canonicalJson: stable key order', () => { + const a = canonicalJson({ b: 2, a: 1 }); + const b = canonicalJson({ a: 1, b: 2 }); + assert.equal(a, b); + assert.equal(a, '{"a":1,"b":2}'); +}); + +void test('canonicalJson: rejects non-finite number', () => { + assert.throws(() => canonicalJson({ x: NaN }), TypeError); +}); diff --git a/test/signPayload.test.mjs b/test/signPayload.test.mjs new file mode 100644 index 0000000..8083513 --- /dev/null +++ b/test/signPayload.test.mjs @@ -0,0 +1,18 @@ +import assert from 'node:assert/strict'; +import { generateKeyPairSync, verify } from 'node:crypto'; +import test from 'node:test'; + +import { signPayload } from '../lib/signPayload.mjs'; + +void test('signPayload produces verifiable Ed25519 token', () => { + const { privateKey, publicKey } = generateKeyPairSync('ed25519'); + const pem = privateKey.export({ type: 'pkcs8', format: 'pem' }); + const payload = { v: 1, sub: 's1', pid: 'p', iat: 1, exp: 2, did: 'd1' }; + const token = signPayload(payload, pem); + const [bodyB64, sigB64] = token.split('.'); + assert.ok(bodyB64 && sigB64); + const body = Buffer.from(bodyB64, 'base64url'); + const sig = Buffer.from(sigB64, 'base64url'); + const ok = verify(null, body, publicKey, sig); + assert.equal(ok, true); +});