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 { 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})`;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
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