forked from FoundKeyGang/FoundKey
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:
parent
de18c8306d
commit
03b673165f
3 changed files with 77 additions and 94 deletions
|
@ -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})`;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
50
packages/backend/src/remote/activitypub/misc/auth-user.ts
Normal file
50
packages/backend/src/remote/activitypub/misc/auth-user.ts
Normal 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);
|
||||
}
|
Loading…
Reference in a new issue