server: refactor "authUser" functions into separate file

They did not really fit into the DbResolver because they may fetch data
from remote instances even though DbResolver is only supposed to access
the database.
This commit is contained in:
Johann150 2022-12-03 23:07:33 +01:00
parent de18c8306d
commit 03b673165f
Signed by untrusted user: Johann150
GPG key ID: 9EE6577A2A06F8F1
3 changed files with 77 additions and 94 deletions

View file

@ -9,12 +9,10 @@ import { apRequestChart, federationChart, instanceChart } from '@/services/chart
import { toPuny, extractDbHost } from '@/misc/convert-host.js';
import { getApId } from '@/remote/activitypub/type.js';
import { fetchInstanceMetadata } from '@/services/fetch-instance-metadata.js';
import DbResolver from '@/remote/activitypub/db-resolver.js';
import { resolvePerson } from '@/remote/activitypub/models/person.js';
import Resolver from '@/remote/activitypub/resolver.js';
import { LdSignature } from '@/remote/activitypub/misc/ld-signature.js';
import { getAuthUser } from '@/remote/activitypub/misc/auth-user.js';
import { StatusError } from '@/misc/fetch.js';
import { CacheableRemoteUser } from '@/models/entities/user.js';
import { UserPublickey } from '@/models/entities/user-publickey.js';
import { InboxJobData } from '@/queue/types.js';
import { shouldBlockInstance } from '@/misc/skipped-instances.js';
@ -43,75 +41,58 @@ export default async (job: Bull.Job<InboxJobData>): Promise<string> => {
return `Old keyId is no longer supported. ${keyIdLower}`;
}
const dbResolver = new DbResolver();
const resolver = new Resolver();
// HTTP-Signature keyIdを元にDBから取得
let authUser: {
user: CacheableRemoteUser;
key: UserPublickey | null;
} | null = await dbResolver.getAuthUserFromKeyId(signature.keyId);
// keyIdでわからなければ、activity.actorを元にDBから取得 || activity.actorを元にリモートから取得
if (authUser == null) {
try {
authUser = await dbResolver.getAuthUserFromApId(getApId(activity.actor));
} catch (e) {
// 対象が4xxならスキップ
if (e instanceof StatusError) {
if (e.isClientError) {
return `skip: Ignored deleted actors on both ends ${activity.actor} - ${e.statusCode}`;
}
let authUser;
try {
authUser = await getAuthUser(signature.keyId, getApId(activity.actor), resolver);
} catch (e) {
if (e instanceof StatusError) {
if (e.isClientError) {
return `skip: Ignored deleted actors on both ends ${activity.actor} - ${e.statusCode}`;
} else {
throw new Error(`Error in actor ${activity.actor} - ${e.statusCode || e}`);
}
}
}
// それでもわからなければ終了
if (authUser == null) {
// Key not found? Unacceptable!
return 'skip: failed to resolve user';
} else {
// Found key!
}
// publicKey がなくても終了
if (authUser.key == null) {
return 'skip: failed to resolve user publicKey';
}
// HTTP-Signatureの検証
// verify the HTTP Signature
const httpSignatureValidated = httpSignature.verifySignature(signature, authUser.key.keyPem);
// また、signatureのsignerは、activity.actorと一致する必要がある
// The signature must be valid.
// The signature must also match the actor otherwise anyone could sign any activity.
if (!httpSignatureValidated || authUser.user.uri !== activity.actor) {
// 一致しなくても、でもLD-Signatureがありそうならそっちも見る
// Last resort: LD-Signature
if (activity.signature) {
if (activity.signature.type !== 'RsaSignature2017') {
return `skip: unsupported LD-signature type ${activity.signature.type}`;
}
// activity.signature.creator: https://example.oom/users/user#main-key
// みたいになっててUserを引っ張れば公開キーも入ることを期待する
if (activity.signature.creator) {
const candicate = activity.signature.creator.replace(/#.*/, '');
await resolvePerson(candicate).catch(() => null);
}
// get user based on LD-Signature key id.
// lets assume that the creator has this common form:
// <https://example.com/users/user#main-key>
// Then we can use it as the key id and (without fragment part) user id.
authUser = await getAuthUser(activity.signature.creator, activity.signature.creator.replace(/#.*$/, ''));
// keyIdからLD-Signatureのユーザーを取得
authUser = await dbResolver.getAuthUserFromKeyId(activity.signature.creator);
if (authUser == null) {
return 'skip: LD-Signatureのユーザーが取得できませんでした';
return 'skip: failed to resolve LD-Signature user';
}
if (authUser.key == null) {
return 'skip: LD-SignatureのユーザーはpublicKeyを持っていませんでした';
}
// LD-Signature検証
// LD-Signature verification
const ldSignature = new LdSignature();
const verified = await ldSignature.verifyRsaSignature2017(activity, authUser.key.keyPem).catch(() => false);
if (!verified) {
return 'skip: LD-Signatureの検証に失敗しました';
}
// もう一度actorチェック
// Again, the actor must match.
if (authUser.user.uri !== activity.actor) {
return `skip: LD-Signature user(${authUser.user.uri}) !== activity.actor(${activity.actor})`;
}

View file

@ -2,22 +2,10 @@ import escapeRegexp from 'escape-regexp';
import config from '@/config/index.js';
import { Note } from '@/models/entities/note.js';
import { CacheableRemoteUser, CacheableUser } from '@/models/entities/user.js';
import { UserPublickey } from '@/models/entities/user-publickey.js';
import { MessagingMessage } from '@/models/entities/messaging-message.js';
import { Notes, Users, UserPublickeys, MessagingMessages } from '@/models/index.js';
import { Cache } from '@/misc/cache.js';
import { Notes, MessagingMessages } from '@/models/index.js';
import { uriPersonCache, userByIdCache } from '@/services/user-cache.js';
import { IObject, getApId } from './type.js';
import { resolvePerson } from './models/person.js';
const publicKeyCache = new Cache<UserPublickey>(
Infinity,
(keyId) => UserPublickeys.findOneBy({ keyId }).then(x => x ?? undefined),
);
const publicKeyByUserIdCache = new Cache<UserPublickey>(
Infinity,
(userId) => UserPublickeys.findOneBy({ userId }).then(x => x ?? undefined),
);
export type UriParseResult = {
/** wether the URI was generated by us */
@ -110,40 +98,4 @@ export default class DbResolver {
return await uriPersonCache.fetch(parsed.uri) ?? null;
}
}
/**
* AP KeyId => FoundKey User and Key
*/
public async getAuthUserFromKeyId(keyId: string): Promise<{
user: CacheableRemoteUser;
key: UserPublickey;
} | null> {
const key = await publicKeyCache.fetch(keyId);
if (key == null) return null;
return {
user: await userByIdCache.fetch(key.userId) as CacheableRemoteUser,
key,
};
}
/**
* AP Actor id => FoundKey User and Key
*/
public async getAuthUserFromApId(uri: string): Promise<{
user: CacheableRemoteUser;
key: UserPublickey | null;
} | null> {
const user = await resolvePerson(uri) as CacheableRemoteUser;
if (user == null) return null;
const key = await publicKeyByUserIdCache.fetch(user.id);
return {
user,
key,
};
}
}

View file

@ -0,0 +1,50 @@
import { Cache } from '@/misc/cache.js';
import { UserPublickeys } from '@/models/index.js';
import { CacheableRemoteUser } from '@/models/entities/user.js';
import { UserPublickey } from '@/models/entities/user-publickey.js';
import { uriPersonCache, userByIdCache } from '@/services/user-cache.js';
import { createPerson } from '@/remote/activitypub/models/person.js';
export type AuthUser = {
user: CacheableRemoteUser;
key: UserPublickey;
};
const publicKeyCache = new Cache<UserPublickey>(
Infinity,
(keyId) => UserPublickeys.findOneBy({ keyId }).then(x => x ?? undefined),
);
const publicKeyByUserIdCache = new Cache<UserPublickey>(
Infinity,
(userId) => UserPublickeys.findOneBy({ userId }).then(x => x ?? undefined),
);
function authUserFromApId(uri: string): Promise<AuthUser | null> {
return uriPersonCache.fetch(uri)
.then(async user => {
if (!user) return null;
let key = await publicKeyByUserIdCache.fetch(user.id);
if (!key) return null;
return { user, key };
});
}
export async function getAuthUser(keyId: string, actorUri: string, resolver: Resolver): Promise<AuthUser | null> {
let authUser = await publicKeyCache.fetch(keyId)
.then(key => {
if (!key) return null;
else return {
user: await userByIdCache.fetch(key.userId),
key,
};
});
if (authUser != null) return authUser;
authUser = await authUserFromApId(actorUri);
if (authUser != null) return authUser;
// fetch from remote and then one last try
await createPerson(actorUri, resolver);
// if this one still returns null it seems this user really does not exist
return await authUserFromApId(actorUri);
}