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: 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 { toPuny, extractDbHost } from '@/misc/convert-host.js';
import { getApId } from '@/remote/activitypub/type.js'; import { getApId } from '@/remote/activitypub/type.js';
import { fetchInstanceMetadata } from '@/services/fetch-instance-metadata.js'; import { fetchInstanceMetadata } from '@/services/fetch-instance-metadata.js';
import DbResolver from '@/remote/activitypub/db-resolver.js'; import Resolver from '@/remote/activitypub/resolver.js';
import { resolvePerson } from '@/remote/activitypub/models/person.js';
import { LdSignature } from '@/remote/activitypub/misc/ld-signature.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 { 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 { InboxJobData } from '@/queue/types.js';
import { shouldBlockInstance } from '@/misc/skipped-instances.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}`; return `Old keyId is no longer supported. ${keyIdLower}`;
} }
const dbResolver = new DbResolver(); const resolver = new Resolver();
// HTTP-Signature keyIdを元にDBから取得 let authUser;
let authUser: {
user: CacheableRemoteUser;
key: UserPublickey | null;
} | null = await dbResolver.getAuthUserFromKeyId(signature.keyId);
// keyIdでわからなければ、activity.actorを元にDBから取得 || activity.actorを元にリモートから取得
if (authUser == null) {
try { try {
authUser = await dbResolver.getAuthUserFromApId(getApId(activity.actor)); authUser = await getAuthUser(signature.keyId, getApId(activity.actor), resolver);
} catch (e) { } catch (e) {
// 対象が4xxならスキップ
if (e instanceof StatusError) { if (e instanceof StatusError) {
if (e.isClientError) { if (e.isClientError) {
return `skip: Ignored deleted actors on both ends ${activity.actor} - ${e.statusCode}`; return `skip: Ignored deleted actors on both ends ${activity.actor} - ${e.statusCode}`;
} } else {
throw new Error(`Error in actor ${activity.actor} - ${e.statusCode || e}`); throw new Error(`Error in actor ${activity.actor} - ${e.statusCode || e}`);
} }
} }
} }
// それでもわからなければ終了
if (authUser == null) { if (authUser == null) {
// Key not found? Unacceptable!
return 'skip: failed to resolve user'; return 'skip: failed to resolve user';
} else {
// Found key!
} }
// publicKey がなくても終了 // verify the HTTP Signature
if (authUser.key == null) {
return 'skip: failed to resolve user publicKey';
}
// HTTP-Signatureの検証
const httpSignatureValidated = httpSignature.verifySignature(signature, authUser.key.keyPem); 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) { if (!httpSignatureValidated || authUser.user.uri !== activity.actor) {
// 一致しなくても、でもLD-Signatureがありそうならそっちも見る // Last resort: LD-Signature
if (activity.signature) { if (activity.signature) {
if (activity.signature.type !== 'RsaSignature2017') { if (activity.signature.type !== 'RsaSignature2017') {
return `skip: unsupported LD-signature type ${activity.signature.type}`; return `skip: unsupported LD-signature type ${activity.signature.type}`;
} }
// activity.signature.creator: https://example.oom/users/user#main-key // get user based on LD-Signature key id.
// みたいになっててUserを引っ張れば公開キーも入ることを期待する // lets assume that the creator has this common form:
if (activity.signature.creator) { // <https://example.com/users/user#main-key>
const candicate = activity.signature.creator.replace(/#.*/, ''); // Then we can use it as the key id and (without fragment part) user id.
await resolvePerson(candicate).catch(() => null); authUser = await getAuthUser(activity.signature.creator, activity.signature.creator.replace(/#.*$/, ''));
}
// keyIdからLD-Signatureのユーザーを取得
authUser = await dbResolver.getAuthUserFromKeyId(activity.signature.creator);
if (authUser == null) { if (authUser == null) {
return 'skip: LD-Signatureのユーザーが取得できませんでした'; return 'skip: failed to resolve LD-Signature user';
} }
if (authUser.key == null) { // LD-Signature verification
return 'skip: LD-SignatureのユーザーはpublicKeyを持っていませんでした';
}
// LD-Signature検証
const ldSignature = new LdSignature(); const ldSignature = new LdSignature();
const verified = await ldSignature.verifyRsaSignature2017(activity, authUser.key.keyPem).catch(() => false); const verified = await ldSignature.verifyRsaSignature2017(activity, authUser.key.keyPem).catch(() => false);
if (!verified) { if (!verified) {
return 'skip: LD-Signatureの検証に失敗しました'; return 'skip: LD-Signatureの検証に失敗しました';
} }
// もう一度actorチェック // Again, the actor must match.
if (authUser.user.uri !== activity.actor) { if (authUser.user.uri !== activity.actor) {
return `skip: LD-Signature user(${authUser.user.uri}) !== activity.actor(${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 config from '@/config/index.js';
import { Note } from '@/models/entities/note.js'; import { Note } from '@/models/entities/note.js';
import { CacheableRemoteUser, CacheableUser } from '@/models/entities/user.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 { MessagingMessage } from '@/models/entities/messaging-message.js';
import { Notes, Users, UserPublickeys, MessagingMessages } from '@/models/index.js'; import { Notes, MessagingMessages } from '@/models/index.js';
import { Cache } from '@/misc/cache.js';
import { uriPersonCache, userByIdCache } from '@/services/user-cache.js'; import { uriPersonCache, userByIdCache } from '@/services/user-cache.js';
import { IObject, getApId } from './type.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 = { export type UriParseResult = {
/** wether the URI was generated by us */ /** wether the URI was generated by us */
@ -110,40 +98,4 @@ export default class DbResolver {
return await uriPersonCache.fetch(parsed.uri) ?? null; 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);
}