feat: http signature checking
parent
ef982be2d7
commit
61fc756255
|
@ -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;
|
||||
}
|
|
@ -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']})`
|
|
@ -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}`)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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",
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
|
|
Reference in New Issue