feat: http signature checking

main
xtexChooser 2022-12-10 15:05:32 +08:00
parent ef982be2d7
commit 61fc756255
No known key found for this signature in database
GPG Key ID: 978F2E760D9DB0EB
7 changed files with 180 additions and 12 deletions

77
@types/http-signature.d.ts vendored Normal file
View File

@ -0,0 +1,77 @@
declare module '@peertube/http-signature' {
import type { IncomingMessage, ClientRequest } from 'node:http';
interface ISignature {
keyId: string;
algorithm: string;
headers: string[];
signature: string;
}
interface IOptions {
headers?: string[];
algorithm?: string;
strict?: boolean;
authorizationHeaderName?: string;
}
interface IParseRequestOptions extends IOptions {
clockSkew?: number;
}
interface IParsedSignature {
scheme: string;
params: ISignature;
signingString: string;
algorithm: string;
keyId: string;
}
type RequestSignerConstructorOptions =
IRequestSignerConstructorOptionsFromProperties |
IRequestSignerConstructorOptionsFromFunction;
interface IRequestSignerConstructorOptionsFromProperties {
keyId: string;
key: string | Buffer;
algorithm?: string;
}
interface IRequestSignerConstructorOptionsFromFunction {
sign?: (data: string, cb: (err: any, sig: ISignature) => void) => void;
}
class RequestSigner {
constructor(options: RequestSignerConstructorOptions);
public writeHeader(header: string, value: string): string;
public writeDateHeader(): string;
public writeTarget(method: string, path: string): void;
public sign(cb: (err: any, authz: string) => void): void;
}
interface ISignRequestOptions extends IOptions {
keyId: string;
key: string;
httpVersion?: string;
}
export function parse(request: IncomingMessage, options?: IParseRequestOptions): IParsedSignature;
export function parseRequest(request: IncomingMessage, options?: IParseRequestOptions): IParsedSignature;
export function sign(request: ClientRequest, options: ISignRequestOptions): boolean;
export function signRequest(request: ClientRequest, options: ISignRequestOptions): boolean;
export function createSigner(): RequestSigner;
export function isSigner(obj: any): obj is RequestSigner;
export function sshKeyToPEM(key: string): string;
export function sshKeyFingerprint(key: string): string;
export function pemToRsaSSHKey(pem: string, comment: string): string;
export function verify(parsedSignature: IParsedSignature, pubkey: string | Buffer): boolean;
export function verifySignature(parsedSignature: IParsedSignature, pubkey: string | Buffer): boolean;
export function verifyHMAC(parsedSignature: IParsedSignature, secret: string): boolean;
}

4
ap/consts.ts Normal file
View File

@ -0,0 +1,4 @@
import { env } from "process"
export const ACCEPT_HEADER = 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'
export const USER_AGENT = `xtex-home/1.0@${env['VERCEL_GIT_COMMIT_SHA']} (${env['NEXT_PUBLIC_VERCEL_URL']})`

35
ap/delivery.ts Normal file
View File

@ -0,0 +1,35 @@
import { signRequest } from "@peertube/http-signature";
import { BaseEntity } from "activitypub-core-types/lib/activitypub/Core/Entity"
import { ClientRequest } from "http";
import { env } from "process";
import { ACCEPT_HEADER, USER_AGENT } from "./consts"
export async function deliveryAPActivity(url: URL, doc: BaseEntity) {
console.log(`deliverying AP document ${url}`)
const request = {
url: url.toString(),
method: 'POST',
headers: {
'Date': new Date().toUTCString(),
'Host': url.hostname,
},
}
const signature = signRequest(request as unknown as ClientRequest, {
key: env['XTEX_HOME_AP_PRIV_KEY']!!,
keyId: 'XTEX-HOME-AP-INSTANCE-ACTOR',
})
console.log(signature)
const result = await fetch(url, {
method: 'POST',
body: JSON.stringify(doc),
headers: {
'User-Agent': USER_AGENT,
'Accept': ACCEPT_HEADER,
'Content-Type': 'application/activity+json',
}
})
console.log(`delivered AP doc ${url}: ${result.status}`)
}

29
ap/resolver.ts Normal file
View File

@ -0,0 +1,29 @@
import { CoreObject, Entity, EntityReference, Link } from "activitypub-core-types/lib/activitypub";
import { BaseEntity } from "activitypub-core-types/lib/activitypub/Core/Entity";
import { URL } from "url";
import { ACCEPT_HEADER, USER_AGENT } from "./consts";
export async function resolveApEntity(ref: EntityReference): Promise<Entity> {
if (typeof ref == 'string') {
return await getApDocument(ref) as Entity
} else if (typeof (ref as Link).href == 'string') {
return resolveApEntity((ref as Link).href!!)
} else if (typeof (ref as CoreObject).type == 'string') {
return ref as CoreObject
} else {
throw `${ref} cannot be resolved as a AP entity`
}
}
export async function getApDocument(url: URL): Promise<BaseEntity> {
console.log(`resolving AP document ${url}`)
const result = await fetch(url, {
method: 'GET',
headers: {
'User-Agent': USER_AGENT,
'Accept': ACCEPT_HEADER,
}
})
console.log(`resolved AP doc ${url}: ${result.status}`)
return (await result.json()) as BaseEntity
}

View File

@ -9,7 +9,7 @@
"lint": "next lint"
},
"dependencies": {
"@mattrglobal/http-signatures": "4.0.1",
"@peertube/http-signature": "1.7.0",
"@types/node": "18.11.9",
"@types/react": "18.0.25",
"@types/react-dom": "18.0.8",

View File

@ -1,13 +1,39 @@
import * as httpSignature from '@peertube/http-signature'
import { Activity, ActivityTypes, Actor, Reject } from 'activitypub-core-types/lib/activitypub'
import type { NextApiRequest, NextApiResponse } from 'next'
import { deliveryAPActivity } from '../../../ap/delivery'
import { resolveApEntity } from '../../../ap/resolver'
type Data = {
}
export default function handler(
export default async function handler(
req: NextApiRequest,
res: NextApiResponse<Data>
res: NextApiResponse
) {
console.log(req.body)
console.log(JSON.stringify(req.body))
const signature = req.headers['signature']
const activity = req.body as Activity
if (signature == null) {
return res.status(400).send('no signature')
}
if (activity.actor instanceof Array) {
return res.status(400).send('actor is more than one AS entity ref')
}
const actor = (await resolveApEntity(activity.actor) as Actor)
if (!httpSignature.verifySignature(httpSignature.parseRequest(req), actor.publicKey!!.publicKeyPem)) {
console.error(`signature check failed, actor: ${actor}, provided: ${signature}`)
return res.status(400).send(`signature check failed, expected: ${actor.publicKey}`)
}
if (activity.type == ActivityTypes.FOLLOW) {
// follow request
console.log(`sending follow Reject to ${actor.inbox}`)
if (actor.inbox! instanceof URL) {
return res.status(400).send('inbox is not a standalone doc')
}
await deliveryAPActivity(actor.inbox as unknown as URL, {
type: ActivityTypes.REJECT,
id: new URL(`https://xtexx.ml/ap/reject_follows/${encodeURI(actor.id?.toString())}`),
actor: new URL('https://xtexx.ml/ap/actor.json'),
object: activity.id,
target: actor.id,
} as Reject)
}
res.status(200).end()
}

View File

@ -1,11 +1,8 @@
import type { NextApiRequest, NextApiResponse } from 'next'
type Data = {
}
export default function handler(
req: NextApiRequest,
res: NextApiResponse<Data>
res: NextApiResponse
) {
console.log(req.body)
console.log(JSON.stringify(req.body))