use @misskey-dev/node-http-message-signatures
This commit is contained in:
parent
a4e7d6940b
commit
a1e6cb02b8
8 changed files with 45 additions and 211 deletions
|
@ -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",
|
||||||
|
|
82
packages/backend/src/@types/http-signature.d.ts
vendored
82
packages/backend/src/@types/http-signature.d.ts
vendored
|
@ -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;
|
|
||||||
}
|
|
|
@ -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,
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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 = {
|
||||||
|
|
|
@ -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,63 +103,29 @@ 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) {
|
||||||
|| request.headers.host !== this.config.host) {
|
|
||||||
// Host not specified or not match.
|
|
||||||
reply.code(401);
|
reply.code(401);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (signature.params.headers.indexOf('digest') === -1) {
|
if (signature.value.params.headers.indexOf('host') === -1
|
||||||
// Digest not found.
|
|| request.headers.host !== this.config.host) {
|
||||||
|
// Host not specified or not match.
|
||||||
reply.code(401);
|
reply.code(401);
|
||||||
} else {
|
return;
|
||||||
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);
|
||||||
|
|
|
@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -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'}
|
||||||
|
|
Loading…
Reference in a new issue