use @misskey-dev/node-http-message-signatures

This commit is contained in:
tamaina 2024-02-29 21:05:31 +00:00
parent a4e7d6940b
commit a1e6cb02b8
8 changed files with 45 additions and 211 deletions

View file

@ -79,12 +79,12 @@
"@fastify/multipart": "8.1.0", "@fastify/multipart": "8.1.0",
"@fastify/static": "6.12.0", "@fastify/static": "6.12.0",
"@fastify/view": "8.2.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/sharp-read-bmp": "1.2.0",
"@misskey-dev/summaly": "5.0.3", "@misskey-dev/summaly": "5.0.3",
"@nestjs/common": "10.2.10", "@nestjs/common": "10.2.10",
"@nestjs/core": "10.2.10", "@nestjs/core": "10.2.10",
"@nestjs/testing": "10.2.10", "@nestjs/testing": "10.2.10",
"@peertube/http-signature": "1.7.0",
"@simplewebauthn/server": "9.0.3", "@simplewebauthn/server": "9.0.3",
"@sinonjs/fake-timers": "11.2.2", "@sinonjs/fake-timers": "11.2.2",
"@smithy/node-http-handler": "2.1.10", "@smithy/node-http-handler": "2.1.10",

View file

@ -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;
}

View file

@ -12,11 +12,11 @@ import type { Config } from '@/config.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import type { Antenna } from '@/server/api/endpoints/i/import-antennas.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 { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, RelationshipQueue, SystemQueue, WebhookDeliverQueue } from './QueueModule.js';
import type { DbJobData, DeliverJobData, RelationshipJobData, ThinUser } from '../queue/types.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 type * as Bull from 'bullmq';
import { ApRequestCreator } from '@/core/activitypub/ApRequestService.js'; import type { ParsedSignature } from '@misskey-dev/node-http-message-signatures';
@Injectable() @Injectable()
export class QueueService { export class QueueService {
@ -136,7 +136,7 @@ export class QueueService {
} }
@bindThis @bindThis
public inbox(activity: IActivity, signature: httpSignature.IParsedSignature) { public inbox(activity: IActivity, signature: ParsedSignature) {
const data = { const data = {
activity: activity, activity: activity,
signature, signature,

View file

@ -5,8 +5,8 @@
import { URL } from 'node:url'; import { URL } from 'node:url';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import httpSignature from '@peertube/http-signature';
import * as Bull from 'bullmq'; import * as Bull from 'bullmq';
import { verifyDraftSignature } from '@misskey-dev/node-http-message-signatures';
import type Logger from '@/logger.js'; import type Logger from '@/logger.js';
import { MetaService } from '@/core/MetaService.js'; import { MetaService } from '@/core/MetaService.js';
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
@ -51,7 +51,7 @@ export class InboxProcessorService {
@bindThis @bindThis
public async process(job: Bull.Job<InboxJobData>): Promise<string> { public async process(job: Bull.Job<InboxJobData>): Promise<string> {
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; const activity = job.data.activity;
//#region Log //#region Log
@ -103,7 +103,7 @@ export class InboxProcessorService {
} }
// HTTP-Signatureの検証 // HTTP-Signatureの検証
const httpSignatureValidated = httpSignature.verifySignature(signature, authUser.key.keyPem); const httpSignatureValidated = verifyDraftSignature(signature, authUser.key.keyPem);
// また、signatureのsignerは、activity.actorと一致する必要がある // また、signatureのsignerは、activity.actorと一致する必要がある
if (!httpSignatureValidated || authUser.user.uri !== activity.actor) { if (!httpSignatureValidated || authUser.user.uri !== activity.actor) {

View file

@ -9,7 +9,23 @@ import type { MiNote } from '@/models/Note.js';
import type { MiUser } from '@/models/User.js'; import type { MiUser } from '@/models/User.js';
import type { MiWebhook } from '@/models/Webhook.js'; import type { MiWebhook } from '@/models/Webhook.js';
import type { IActivity } from '@/core/activitypub/type.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 = { export type DeliverJobData = {
/** Actor */ /** Actor */
@ -26,7 +42,7 @@ export type DeliverJobData = {
export type InboxJobData = { export type InboxJobData = {
activity: IActivity; activity: IActivity;
signature: httpSignature.IParsedSignature; signature: ParsedSignature | OldParsedSignature;
}; };
export type RelationshipJobData = { export type RelationshipJobData = {

View file

@ -7,7 +7,7 @@ import * as crypto from 'node:crypto';
import { IncomingMessage } from 'node:http'; import { IncomingMessage } from 'node:http';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import fastifyAccepts from '@fastify/accepts'; 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 { Brackets, In, IsNull, LessThan, Not } from 'typeorm';
import accepts from 'accepts'; import accepts from 'accepts';
import vary from 'vary'; import vary from 'vary';
@ -103,65 +103,31 @@ export class ActivityPubServerService {
private inbox(request: FastifyRequest, reply: FastifyReply) { private inbox(request: FastifyRequest, reply: FastifyReply) {
let signature; let signature;
const verifyDigest = verifyDigestHeader(request.raw, request.rawBody || '', true);
if (!verifyDigest) {
reply.code(401);
return;
}
try { try {
signature = httpSignature.parseRequest(request.raw, { 'headers': [] }); signature = parseRequestSignature(request.raw);
} catch (e) { } catch (e) {
reply.code(401); reply.code(401);
return; return;
} }
if (signature.params.headers.indexOf('host') === -1 if (!signature) {
reply.code(401);
return;
}
if (signature.value.params.headers.indexOf('host') === -1
|| request.headers.host !== this.config.host) { || request.headers.host !== this.config.host) {
// Host not specified or not match. // Host not specified or not match.
reply.code(401); reply.code(401);
return; return;
} }
if (signature.params.headers.indexOf('digest') === -1) {
// Digest not found.
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;
}
}
this.queueService.inbox(request.body as IActivity, signature); this.queueService.inbox(request.body as IActivity, signature);
reply.code(202); reply.code(202);

View file

@ -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);
});
});

View file

@ -110,6 +110,9 @@ importers:
'@fastify/view': '@fastify/view':
specifier: 8.2.0 specifier: 8.2.0
version: 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': '@misskey-dev/sharp-read-bmp':
specifier: 1.2.0 specifier: 1.2.0
version: 1.2.0 version: 1.2.0
@ -125,9 +128,6 @@ importers:
'@nestjs/testing': '@nestjs/testing':
specifier: 10.2.10 specifier: 10.2.10
version: 10.2.10(@nestjs/common@10.2.10)(@nestjs/core@10.2.10)(@nestjs/platform-express@10.3.3) 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': '@simplewebauthn/server':
specifier: 9.0.3 specifier: 9.0.3
version: 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) eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.18.1)(eslint@8.56.0)
dev: true 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: /@misskey-dev/sharp-read-bmp@1.2.0:
resolution: {integrity: sha512-er4pRakXzHYfEgOFAFfQagqDouG+wLm+kwNq1I30oSdIHDa0wM3KjFpfIGQ25Fks4GcmOl1s7Zh6xoQu5dNjTw==} resolution: {integrity: sha512-er4pRakXzHYfEgOFAFfQagqDouG+wLm+kwNq1I30oSdIHDa0wM3KjFpfIGQ25Fks4GcmOl1s7Zh6xoQu5dNjTw==}
dependencies: dependencies:
@ -5057,15 +5061,6 @@ packages:
tslib: 2.6.2 tslib: 2.6.2
dev: false 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: /@pkgjs/parseargs@0.11.0:
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
engines: {node: '>=14'} engines: {node: '>=14'}