signedPost, signedGet

This commit is contained in:
tamaina 2024-02-29 22:20:48 +00:00
parent fc20ef0181
commit 735714d61c
6 changed files with 109 additions and 130 deletions

View File

@ -0,0 +1,16 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class HttpSignImplLv1709242519122 {
name = 'HttpSignImplLv1709242519122'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "instance" ADD "httpMessageSignaturesImplementationLevel" character varying(16) NOT NULL DEFAULT '00'`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "instance" DROP COLUMN "httpMessageSignaturesImplementationLevel"`);
}
}

View File

@ -24,6 +24,7 @@ type NodeInfo = {
version?: unknown; version?: unknown;
}; };
metadata?: { metadata?: {
httpMessageSignaturesImplementationLevel?: unknown,
name?: unknown; name?: unknown;
nodeName?: unknown; nodeName?: unknown;
nodeDescription?: unknown; nodeDescription?: unknown;
@ -70,7 +71,7 @@ export class FetchInstanceMetadataService {
if (!force) { if (!force) {
const _instance = await this.federatedInstanceService.fetch(host); const _instance = await this.federatedInstanceService.fetch(host);
const now = Date.now(); const now = Date.now();
if (_instance && _instance.infoUpdatedAt && (now - _instance.infoUpdatedAt.getTime() < 1000 * 60 * 60 * 24)) { if (_instance && _instance.infoUpdatedAt && (now - _instance.infoUpdatedAt.getTime() < 1000 * 60 * 60 * 3)) {
// unlock at the finally caluse // unlock at the finally caluse
return; return;
} }
@ -104,6 +105,9 @@ export class FetchInstanceMetadataService {
updates.openRegistrations = info.openRegistrations; updates.openRegistrations = info.openRegistrations;
updates.maintainerName = info.metadata ? info.metadata.maintainer ? (info.metadata.maintainer.name ?? null) : null : null; updates.maintainerName = info.metadata ? info.metadata.maintainer ? (info.metadata.maintainer.name ?? null) : null : null;
updates.maintainerEmail = info.metadata ? info.metadata.maintainer ? (info.metadata.maintainer.email ?? null) : null : null; updates.maintainerEmail = info.metadata ? info.metadata.maintainer ? (info.metadata.maintainer.email ?? null) : null : null;
if (info.metadata && info.metadata.httpMessageSignaturesImplementationLevel) {
updates.httpMessageSignaturesImplementationLevel = info.metadata.httpMessageSignaturesImplementationLevel.toString() ?? '00';
}
} }
if (name) updates.name = name; if (name) updates.name = name;

View File

@ -3,9 +3,9 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import * as crypto from 'node:crypto';
import { URL } from 'node:url'; import { URL } from 'node:url';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { genRFC3230DigestHeader, RequestLike, signAsDraftToRequest } from '@misskey-dev/node-http-message-signatures';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import type { MiUser } from '@/models/User.js'; import type { MiUser } from '@/models/User.js';
@ -16,12 +16,6 @@ import { bindThis } from '@/decorators.js';
import type Logger from '@/logger.js'; import type Logger from '@/logger.js';
import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js'; import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js';
type Request = {
url: string;
method: string;
headers: Record<string, string>;
};
type Signed = { type Signed = {
request: Request; request: Request;
signingString: string; signingString: string;
@ -34,103 +28,51 @@ type PrivateKey = {
keyId: string; keyId: string;
}; };
export class ApRequestCreator { export function createSignedPost(args: { level: string; key: PrivateKey; url: string; body: string; digest?: string; additionalHeaders: Record<string, string> }) {
static createSignedPost(args: { key: PrivateKey, url: string, body: string, digest?: string, additionalHeaders: Record<string, string> }): Signed { const u = new URL(args.url);
const u = new URL(args.url); const request: RequestLike = {
const digestHeader = args.digest ?? this.createDigest(args.body); url: u.href,
method: 'POST',
headers: {
'Date': new Date().toUTCString(),
'Host': u.host,
'Content-Type': 'application/activity+json',
...args.additionalHeaders,
},
};
const request: Request = { // TODO: levelによって処理を分ける
url: u.href, const digestHeader = args.digest ?? genRFC3230DigestHeader(args.body);
method: 'POST', request.headers['Digest'] = digestHeader;
headers: this.#objectAssignWithLcKey({
'Date': new Date().toUTCString(),
'Host': u.host,
'Content-Type': 'application/activity+json',
'Digest': digestHeader,
}, args.additionalHeaders),
};
const result = this.#signToRequest(request, args.key, ['(request-target)', 'date', 'host', 'digest']); const result = signAsDraftToRequest(request, args.key, ['(request-target)', 'date', 'host', 'digest']);
return { return {
request, request,
signingString: result.signingString, ...result,
signature: result.signature, };
signatureHeader: result.signatureHeader, }
};
}
static createDigest(body: string) { export function createSignedGet(args: { level: string; key: PrivateKey; url: string; additionalHeaders: Record<string, string> }) {
return `SHA-256=${crypto.createHash('sha256').update(body).digest('base64')}`; const u = new URL(args.url);
} const request: RequestLike = {
url: u.href,
method: 'GET',
headers: {
'Accept': 'application/activity+json, application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
'Date': new Date().toUTCString(),
'Host': new URL(args.url).host,
...args.additionalHeaders,
},
};
static createSignedGet(args: { key: PrivateKey, url: string, additionalHeaders: Record<string, string> }): Signed { // TODO: levelによって処理を分ける
const u = new URL(args.url); const result = signAsDraftToRequest(request, args.key, ['(request-target)', 'date', 'host', 'accept']);
const request: Request = { return {
url: u.href, request,
method: 'GET', ...result,
headers: this.#objectAssignWithLcKey({ };
'Accept': 'application/activity+json, application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
'Date': new Date().toUTCString(),
'Host': new URL(args.url).host,
}, args.additionalHeaders),
};
const result = this.#signToRequest(request, args.key, ['(request-target)', 'date', 'host', 'accept']);
return {
request,
signingString: result.signingString,
signature: result.signature,
signatureHeader: result.signatureHeader,
};
}
static #signToRequest(request: Request, key: PrivateKey, includeHeaders: string[]): Signed {
const signingString = this.#genSigningString(request, includeHeaders);
const signature = crypto.sign('sha256', Buffer.from(signingString), key.privateKeyPem).toString('base64');
const signatureHeader = `keyId="${key.keyId}",algorithm="rsa-sha256",headers="${includeHeaders.join(' ')}",signature="${signature}"`;
request.headers = this.#objectAssignWithLcKey(request.headers, {
Signature: signatureHeader,
});
// node-fetch will generate this for us. if we keep 'Host', it won't change with redirects!
delete request.headers['host'];
return {
request,
signingString,
signature,
signatureHeader,
};
}
static #genSigningString(request: Request, includeHeaders: string[]): string {
request.headers = this.#lcObjectKey(request.headers);
const results: string[] = [];
for (const key of includeHeaders.map(x => x.toLowerCase())) {
if (key === '(request-target)') {
results.push(`(request-target): ${request.method.toLowerCase()} ${new URL(request.url).pathname}`);
} else {
results.push(`${key}: ${request.headers[key]}`);
}
}
return results.join('\n');
}
static #lcObjectKey(src: Record<string, string>): Record<string, string> {
const dst: Record<string, string> = {};
for (const key of Object.keys(src).filter(x => x !== '__proto__' && typeof src[x] === 'string')) dst[key.toLowerCase()] = src[key];
return dst;
}
static #objectAssignWithLcKey(a: Record<string, string>, b: Record<string, string>): Record<string, string> {
return Object.assign(this.#lcObjectKey(a), this.#lcObjectKey(b));
}
} }
@Injectable() @Injectable()
@ -150,16 +92,25 @@ export class ApRequestService {
} }
@bindThis @bindThis
public async signedPost(user: { id: MiUser['id'] }, url: string, object: unknown, digest?: string): Promise<void> { private async getPrivateKey(userId: MiUser['id'], level: string): Promise<PrivateKey> {
const keypair = await this.userKeypairService.getUserKeypair(userId);
return (level !== '00' && keypair.ed25519PrivateKey) ? {
privateKeyPem: keypair.ed25519PrivateKey,
keyId: `${this.config.url}/users/${userId}#ed25519-key`,
} : {
privateKeyPem: keypair.privateKey,
keyId: `${this.config.url}/users/${userId}#main-key`,
};
}
@bindThis
public async signedPost(user: { id: MiUser['id'] }, url: string, object: unknown, level: string, digest?: string): Promise<void> {
const body = typeof object === 'string' ? object : JSON.stringify(object); const body = typeof object === 'string' ? object : JSON.stringify(object);
const keypair = await this.userKeypairService.getUserKeypair(user.id); const req = createSignedPost({
level,
const req = ApRequestCreator.createSignedPost({ key: await this.getPrivateKey(user.id, level),
key: {
privateKeyPem: keypair.privateKey,
keyId: `${this.config.url}/users/${user.id}#main-key`,
},
url, url,
body, body,
digest, digest,
@ -180,14 +131,10 @@ export class ApRequestService {
* @param url URL to fetch * @param url URL to fetch
*/ */
@bindThis @bindThis
public async signedGet(url: string, user: { id: MiUser['id'] }): Promise<unknown> { public async signedGet(url: string, user: { id: MiUser['id'] }, level: string): Promise<unknown> {
const keypair = await this.userKeypairService.getUserKeypair(user.id); const req = createSignedGet({
level,
const req = ApRequestCreator.createSignedGet({ key: await this.getPrivateKey(user.id, level),
key: {
privateKeyPem: keypair.privateKey,
keyId: `${this.config.url}/users/${user.id}#main-key`,
},
url, url,
additionalHeaders: { additionalHeaders: {
}, },

View File

@ -16,6 +16,7 @@ import { UtilityService } from '@/core/UtilityService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { LoggerService } from '@/core/LoggerService.js'; import { LoggerService } from '@/core/LoggerService.js';
import type Logger from '@/logger.js'; import type Logger from '@/logger.js';
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
import { isCollectionOrOrderedCollection } from './type.js'; import { isCollectionOrOrderedCollection } from './type.js';
import { ApDbResolverService } from './ApDbResolverService.js'; import { ApDbResolverService } from './ApDbResolverService.js';
import { ApRendererService } from './ApRendererService.js'; import { ApRendererService } from './ApRendererService.js';
@ -41,6 +42,7 @@ export class Resolver {
private httpRequestService: HttpRequestService, private httpRequestService: HttpRequestService,
private apRendererService: ApRendererService, private apRendererService: ApRendererService,
private apDbResolverService: ApDbResolverService, private apDbResolverService: ApDbResolverService,
private federatedInstanceService: FederatedInstanceService,
private loggerService: LoggerService, private loggerService: LoggerService,
private recursionLimit = 100, private recursionLimit = 100,
) { ) {
@ -103,8 +105,10 @@ export class Resolver {
this.user = await this.instanceActorService.getInstanceActor(); this.user = await this.instanceActorService.getInstanceActor();
} }
const server = await this.federatedInstanceService.fetch(host);
const object = (this.user const object = (this.user
? await this.apRequestService.signedGet(value, this.user) as IObject ? await this.apRequestService.signedGet(value, this.user, server.httpMessageSignaturesImplementationLevel) as IObject
: await this.httpRequestService.getActivityJson(value)) as IObject; : await this.httpRequestService.getActivityJson(value)) as IObject;
if ( if (
@ -200,6 +204,7 @@ export class ApResolverService {
private httpRequestService: HttpRequestService, private httpRequestService: HttpRequestService,
private apRendererService: ApRendererService, private apRendererService: ApRendererService,
private apDbResolverService: ApDbResolverService, private apDbResolverService: ApDbResolverService,
private federatedInstanceService: FederatedInstanceService,
private loggerService: LoggerService, private loggerService: LoggerService,
) { ) {
} }
@ -220,6 +225,7 @@ export class ApResolverService {
this.httpRequestService, this.httpRequestService,
this.apRendererService, this.apRendererService,
this.apDbResolverService, this.apDbResolverService,
this.federatedInstanceService,
this.loggerService, this.loggerService,
); );
} }

View File

@ -149,4 +149,9 @@ export class MiInstance {
length: 16384, default: '', length: 16384, default: '',
}) })
public moderationNote: string; public moderationNote: string;
@Column('varchar', {
length: 16, default: '00', nullable: false,
})
public httpMessageSignaturesImplementationLevel: string;
} }

View File

@ -72,24 +72,25 @@ export class DeliverProcessorService {
} }
try { try {
await this.apRequestService.signedPost(job.data.user, job.data.to, job.data.content, job.data.digest); const _server = await this.federatedInstanceService.fetch(host);
await this.fetchInstanceMetadataService.fetchInstanceMetadata(_server).then(() => {});
const server = await this.federatedInstanceService.fetch(host);
await this.apRequestService.signedPost(job.data.user, job.data.to, job.data.content, server.httpMessageSignaturesImplementationLevel, job.data.digest);
// Update stats // Update stats
this.federatedInstanceService.fetch(host).then(i => { if (server.isNotResponding) {
if (i.isNotResponding) { this.federatedInstanceService.update(server.id, {
this.federatedInstanceService.update(i.id, { isNotResponding: false,
isNotResponding: false, });
}); }
}
this.fetchInstanceMetadataService.fetchInstanceMetadata(i); this.apRequestChart.deliverSucc();
this.apRequestChart.deliverSucc(); this.federationChart.deliverd(server.host, true);
this.federationChart.deliverd(i.host, true);
if (meta.enableChartsForFederatedInstances) { if (meta.enableChartsForFederatedInstances) {
this.instanceChart.requestSent(i.host, true); this.instanceChart.requestSent(server.host, true);
} }
});
return 'Success'; return 'Success';
} catch (res) { } catch (res) {