diff --git a/packages/backend/package.json b/packages/backend/package.json index 9b38fd6228..072b9ece14 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -79,12 +79,12 @@ "@fastify/multipart": "8.1.0", "@fastify/static": "6.12.0", "@fastify/view": "8.2.0", + "@misskey-dev/node-http-message-signatures": "0.0.0-alpha.7", "@misskey-dev/sharp-read-bmp": "1.2.0", "@misskey-dev/summaly": "5.0.3", "@nestjs/common": "10.2.10", "@nestjs/core": "10.2.10", "@nestjs/testing": "10.2.10", - "@peertube/http-signature": "1.7.0", "@simplewebauthn/server": "9.0.3", "@sinonjs/fake-timers": "11.2.2", "@smithy/node-http-handler": "2.1.10", diff --git a/packages/backend/src/@types/http-signature.d.ts b/packages/backend/src/@types/http-signature.d.ts deleted file mode 100644 index 75b62e55f0..0000000000 --- a/packages/backend/src/@types/http-signature.d.ts +++ /dev/null @@ -1,82 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -declare module '@peertube/http-signature' { - import type { IncomingMessage, ClientRequest } from 'node:http'; - - interface ISignature { - keyId: string; - algorithm: string; - headers: string[]; - signature: string; - } - - interface IOptions { - headers?: string[]; - algorithm?: string; - strict?: boolean; - authorizationHeaderName?: string; - } - - interface IParseRequestOptions extends IOptions { - clockSkew?: number; - } - - interface IParsedSignature { - scheme: string; - params: ISignature; - signingString: string; - algorithm: string; - keyId: string; - } - - type RequestSignerConstructorOptions = - IRequestSignerConstructorOptionsFromProperties | - IRequestSignerConstructorOptionsFromFunction; - - interface IRequestSignerConstructorOptionsFromProperties { - keyId: string; - key: string | Buffer; - algorithm?: string; - } - - interface IRequestSignerConstructorOptionsFromFunction { - sign?: (data: string, cb: (err: any, sig: ISignature) => void) => void; - } - - class RequestSigner { - constructor(options: RequestSignerConstructorOptions); - - public writeHeader(header: string, value: string): string; - - public writeDateHeader(): string; - - public writeTarget(method: string, path: string): void; - - public sign(cb: (err: any, authz: string) => void): void; - } - - interface ISignRequestOptions extends IOptions { - keyId: string; - key: string; - httpVersion?: string; - } - - export function parse(request: IncomingMessage, options?: IParseRequestOptions): IParsedSignature; - export function parseRequest(request: IncomingMessage, options?: IParseRequestOptions): IParsedSignature; - - export function sign(request: ClientRequest, options: ISignRequestOptions): boolean; - export function signRequest(request: ClientRequest, options: ISignRequestOptions): boolean; - export function createSigner(): RequestSigner; - export function isSigner(obj: any): obj is RequestSigner; - - export function sshKeyToPEM(key: string): string; - export function sshKeyFingerprint(key: string): string; - export function pemToRsaSSHKey(pem: string, comment: string): string; - - export function verify(parsedSignature: IParsedSignature, pubkey: string | Buffer): boolean; - export function verifySignature(parsedSignature: IParsedSignature, pubkey: string | Buffer): boolean; - export function verifyHMAC(parsedSignature: IParsedSignature, secret: string): boolean; -} diff --git a/packages/backend/src/core/QueueService.ts b/packages/backend/src/core/QueueService.ts index c258a22927..ef2ec0d2ff 100644 --- a/packages/backend/src/core/QueueService.ts +++ b/packages/backend/src/core/QueueService.ts @@ -12,11 +12,11 @@ import type { Config } from '@/config.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; import type { Antenna } from '@/server/api/endpoints/i/import-antennas.js'; +import { ApRequestCreator } from '@/core/activitypub/ApRequestService.js'; import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, RelationshipQueue, SystemQueue, WebhookDeliverQueue } from './QueueModule.js'; import type { DbJobData, DeliverJobData, RelationshipJobData, ThinUser } from '../queue/types.js'; -import type httpSignature from '@peertube/http-signature'; import type * as Bull from 'bullmq'; -import { ApRequestCreator } from '@/core/activitypub/ApRequestService.js'; +import type { ParsedSignature } from '@misskey-dev/node-http-message-signatures'; @Injectable() export class QueueService { @@ -136,7 +136,7 @@ export class QueueService { } @bindThis - public inbox(activity: IActivity, signature: httpSignature.IParsedSignature) { + public inbox(activity: IActivity, signature: ParsedSignature) { const data = { activity: activity, signature, diff --git a/packages/backend/src/queue/processors/InboxProcessorService.ts b/packages/backend/src/queue/processors/InboxProcessorService.ts index ac7f8e6cd9..85ce6417b8 100644 --- a/packages/backend/src/queue/processors/InboxProcessorService.ts +++ b/packages/backend/src/queue/processors/InboxProcessorService.ts @@ -5,8 +5,8 @@ import { URL } from 'node:url'; import { Injectable } from '@nestjs/common'; -import httpSignature from '@peertube/http-signature'; import * as Bull from 'bullmq'; +import { verifyDraftSignature } from '@misskey-dev/node-http-message-signatures'; import type Logger from '@/logger.js'; import { MetaService } from '@/core/MetaService.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; @@ -51,7 +51,7 @@ export class InboxProcessorService { @bindThis public async process(job: Bull.Job): Promise { - const signature = job.data.signature; // HTTP-signature + const signature = 'version' in job.data.signature ? job.data.signature.value : job.data.signature; const activity = job.data.activity; //#region Log @@ -103,7 +103,7 @@ export class InboxProcessorService { } // HTTP-Signatureの検証 - const httpSignatureValidated = httpSignature.verifySignature(signature, authUser.key.keyPem); + const httpSignatureValidated = verifyDraftSignature(signature, authUser.key.keyPem); // また、signatureのsignerは、activity.actorと一致する必要がある if (!httpSignatureValidated || authUser.user.uri !== activity.actor) { diff --git a/packages/backend/src/queue/types.ts b/packages/backend/src/queue/types.ts index ce57ba745e..135bccb60c 100644 --- a/packages/backend/src/queue/types.ts +++ b/packages/backend/src/queue/types.ts @@ -9,7 +9,23 @@ import type { MiNote } from '@/models/Note.js'; import type { MiUser } from '@/models/User.js'; import type { MiWebhook } from '@/models/Webhook.js'; import type { IActivity } from '@/core/activitypub/type.js'; -import type httpSignature from '@peertube/http-signature'; +import type { ParsedSignature } from '@misskey-dev/node-http-message-signatures'; + +/** + * @peertube/http-signature 時代の古いデータにも対応しておく + */ +export interface OldParsedSignature { + scheme: 'Signature'; + params: { + keyId: string; + algorithm: string; + headers: string[]; + signature: string; + }; + signingString: string; + algorithm: string; + keyId: string; +} export type DeliverJobData = { /** Actor */ @@ -26,7 +42,7 @@ export type DeliverJobData = { export type InboxJobData = { activity: IActivity; - signature: httpSignature.IParsedSignature; + signature: ParsedSignature | OldParsedSignature; }; export type RelationshipJobData = { diff --git a/packages/backend/src/server/ActivityPubServerService.ts b/packages/backend/src/server/ActivityPubServerService.ts index 5e6cb0a05a..269bc3fb11 100644 --- a/packages/backend/src/server/ActivityPubServerService.ts +++ b/packages/backend/src/server/ActivityPubServerService.ts @@ -7,7 +7,7 @@ import * as crypto from 'node:crypto'; import { IncomingMessage } from 'node:http'; import { Inject, Injectable } from '@nestjs/common'; import fastifyAccepts from '@fastify/accepts'; -import httpSignature from '@peertube/http-signature'; +import { verifyDigestHeader, parseRequestSignature } from '@misskey-dev/node-http-message-signatures'; import { Brackets, In, IsNull, LessThan, Not } from 'typeorm'; import accepts from 'accepts'; import vary from 'vary'; @@ -103,63 +103,29 @@ export class ActivityPubServerService { private inbox(request: FastifyRequest, reply: FastifyReply) { let signature; + const verifyDigest = verifyDigestHeader(request.raw, request.rawBody || '', true); + if (!verifyDigest) { + reply.code(401); + return; + } + try { - signature = httpSignature.parseRequest(request.raw, { 'headers': [] }); + signature = parseRequestSignature(request.raw); } catch (e) { reply.code(401); return; } - if (signature.params.headers.indexOf('host') === -1 - || request.headers.host !== this.config.host) { - // Host not specified or not match. + if (!signature) { reply.code(401); return; } - if (signature.params.headers.indexOf('digest') === -1) { - // Digest not found. + if (signature.value.params.headers.indexOf('host') === -1 + || request.headers.host !== this.config.host) { + // Host not specified or not match. reply.code(401); - } else { - const digest = request.headers.digest; - - if (typeof digest !== 'string') { - // Huh? - reply.code(401); - return; - } - - const re = /^([a-zA-Z0-9\-]+)=(.+)$/; - const match = digest.match(re); - - if (match == null) { - // Invalid digest - reply.code(401); - return; - } - - const algo = match[1].toUpperCase(); - const digestValue = match[2]; - - if (algo !== 'SHA-256') { - // Unsupported digest algorithm - reply.code(401); - return; - } - - if (request.rawBody == null) { - // Bad request - reply.code(400); - return; - } - - const hash = crypto.createHash('sha256').update(request.rawBody).digest('base64'); - - if (hash !== digestValue) { - // Invalid digest - reply.code(401); - return; - } + return; } this.queueService.inbox(request.body as IActivity, signature); diff --git a/packages/backend/test/unit/ap-request.ts b/packages/backend/test/unit/ap-request.ts deleted file mode 100644 index d3d39240dc..0000000000 --- a/packages/backend/test/unit/ap-request.ts +++ /dev/null @@ -1,61 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import * as assert from 'assert'; -import httpSignature from '@peertube/http-signature'; - -import { genRsaKeyPair } from '@/misc/gen-key-pair.js'; -import { ApRequestCreator } from '@/core/activitypub/ApRequestService.js'; - -export const buildParsedSignature = (signingString: string, signature: string, algorithm: string) => { - return { - scheme: 'Signature', - params: { - keyId: 'KeyID', // dummy, not used for verify - algorithm: algorithm, - headers: ['(request-target)', 'date', 'host', 'digest'], // dummy, not used for verify - signature: signature, - }, - signingString: signingString, - algorithm: algorithm.toUpperCase(), - keyId: 'KeyID', // dummy, not used for verify - }; -}; - -describe('ap-request', () => { - test('createSignedPost with verify', async () => { - const keypair = await genRsaKeyPair(); - const key = { keyId: 'x', 'privateKeyPem': keypair.privateKey }; - const url = 'https://example.com/inbox'; - const activity = { a: 1 }; - const body = JSON.stringify(activity); - const headers = { - 'User-Agent': 'UA', - }; - - const req = ApRequestCreator.createSignedPost({ key, url, body, additionalHeaders: headers }); - - const parsed = buildParsedSignature(req.signingString, req.signature, 'rsa-sha256'); - - const result = httpSignature.verifySignature(parsed, keypair.publicKey); - assert.deepStrictEqual(result, true); - }); - - test('createSignedGet with verify', async () => { - const keypair = await genRsaKeyPair(); - const key = { keyId: 'x', 'privateKeyPem': keypair.privateKey }; - const url = 'https://example.com/outbox'; - const headers = { - 'User-Agent': 'UA', - }; - - const req = ApRequestCreator.createSignedGet({ key, url, additionalHeaders: headers }); - - const parsed = buildParsedSignature(req.signingString, req.signature, 'rsa-sha256'); - - const result = httpSignature.verifySignature(parsed, keypair.publicKey); - assert.deepStrictEqual(result, true); - }); -}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 26add9a11e..922234f34d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -110,6 +110,9 @@ importers: '@fastify/view': specifier: 8.2.0 version: 8.2.0 + '@misskey-dev/node-http-message-signatures': + specifier: 0.0.0-alpha.7 + version: 0.0.0-alpha.7 '@misskey-dev/sharp-read-bmp': specifier: 1.2.0 version: 1.2.0 @@ -125,9 +128,6 @@ importers: '@nestjs/testing': specifier: 10.2.10 version: 10.2.10(@nestjs/common@10.2.10)(@nestjs/core@10.2.10)(@nestjs/platform-express@10.3.3) - '@peertube/http-signature': - specifier: 1.7.0 - version: 1.7.0 '@simplewebauthn/server': specifier: 9.0.3 version: 9.0.3 @@ -4735,6 +4735,10 @@ packages: eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.18.1)(eslint@8.56.0) dev: true + /@misskey-dev/node-http-message-signatures@0.0.0-alpha.7: + resolution: {integrity: sha512-iM1nZ3YT+G4AEhbUnsK7PqnMY9MjBP5JomQAgi2OyxDtZ/wBpgLP6MCVz3ElCqZ8NQS1f+c4E1m6/dSN8MtU9Q==} + dev: false + /@misskey-dev/sharp-read-bmp@1.2.0: resolution: {integrity: sha512-er4pRakXzHYfEgOFAFfQagqDouG+wLm+kwNq1I30oSdIHDa0wM3KjFpfIGQ25Fks4GcmOl1s7Zh6xoQu5dNjTw==} dependencies: @@ -5057,15 +5061,6 @@ packages: tslib: 2.6.2 dev: false - /@peertube/http-signature@1.7.0: - resolution: {integrity: sha512-aGQIwo6/sWtyyqhVK4e1MtxYz4N1X8CNt6SOtCc+Wnczs5S5ONaLHDDR8LYaGn0MgOwvGgXyuZ5sJIfd7iyoUw==} - engines: {node: '>=0.10'} - dependencies: - assert-plus: 1.0.0 - jsprim: 1.4.2 - sshpk: 1.17.0 - dev: false - /@pkgjs/parseargs@0.11.0: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'}