136 lines
4.9 KiB
JavaScript
136 lines
4.9 KiB
JavaScript
import { URL } from "url";
|
|
import httpSignature from "@peertube/http-signature";
|
|
import config from "../../config/index.js";
|
|
import { fetchMeta } from "../../misc/fetch-meta.js";
|
|
import { toPuny } from "../../misc/convert-host.js";
|
|
import DbResolver from "./db-resolver.js";
|
|
import { getApId } from "./type.js";
|
|
import { shouldBlockInstance } from "../../misc/should-block-instance.js";
|
|
import { verify } from "node:crypto";
|
|
import { toSingle } from "../../prelude/array.js";
|
|
import { createHash } from "node:crypto";
|
|
import { tickFetch } from "../../metrics.js";
|
|
export async function hasSignature(req) {
|
|
const meta = await fetchMeta();
|
|
const required = meta.secureMode || meta.privateMode;
|
|
try {
|
|
httpSignature.parseRequest(req, {
|
|
headers: []
|
|
});
|
|
} catch (e) {
|
|
if (e instanceof Error && e.name === "MissingHeaderError") {
|
|
return required ? "missing" : "optional";
|
|
}
|
|
return "invalid";
|
|
}
|
|
return required ? "supplied" : "unneeded";
|
|
}
|
|
export async function checkFetch(req) {
|
|
const meta = await fetchMeta();
|
|
if (meta.secureMode || meta.privateMode) {
|
|
if (req.headers.host !== config.host) return 400;
|
|
let signature;
|
|
try {
|
|
signature = httpSignature.parseRequest(req, {
|
|
headers: [
|
|
"(request-target)",
|
|
"host",
|
|
"date"
|
|
],
|
|
authorizationHeaderName: 'signature'
|
|
});
|
|
} catch (e) {
|
|
return 401;
|
|
}
|
|
const keyId = new URL(signature.keyId);
|
|
const host = toPuny(keyId.hostname);
|
|
if (await shouldBlockInstance(host, meta)) {
|
|
return 403;
|
|
}
|
|
if (meta.privateMode && host !== config.host && host !== config.domain && !meta.allowedHosts.includes(host)) {
|
|
return 403;
|
|
}
|
|
const keyIdLower = signature.keyId.toLowerCase();
|
|
if (keyIdLower.startsWith("acct:")) {
|
|
// Old keyId is no longer supported.
|
|
return 401;
|
|
}
|
|
const dbResolver = new DbResolver();
|
|
// HTTP-Signature keyIdを元にDBから取得
|
|
let authUser = await dbResolver.getAuthUserFromKeyId(signature.keyId);
|
|
// keyIdでわからなければ、resolveしてみる
|
|
if (authUser == null) {
|
|
try {
|
|
keyId.hash = "";
|
|
authUser = await dbResolver.getAuthUserFromApId(getApId(keyId.toString()));
|
|
} catch (e) {
|
|
// できなければ駄目
|
|
return 403;
|
|
}
|
|
}
|
|
// publicKey がなくても終了
|
|
if (authUser?.key == null) {
|
|
return 403;
|
|
}
|
|
// Cannot authenticate against local user
|
|
if (authUser.user.uri === null || authUser.user.host === null) {
|
|
return 400;
|
|
}
|
|
// Check if keyId hostname matches actor hostname
|
|
if (toPuny(new URL(authUser.user.uri).hostname) !== host) {
|
|
return 403;
|
|
}
|
|
// HTTP-Signatureの検証
|
|
let httpSignatureValidated = httpSignature.verifySignature(signature, authUser.key.keyPem);
|
|
// If signature validation failed, try refetching the actor
|
|
if (!httpSignatureValidated) {
|
|
authUser.key = await dbResolver.refetchPublicKeyForApId(authUser.user);
|
|
if (authUser.key == null) {
|
|
return 403;
|
|
}
|
|
httpSignatureValidated = httpSignature.verifySignature(signature, authUser.key.keyPem);
|
|
}
|
|
if (!httpSignatureValidated) {
|
|
return 403;
|
|
}
|
|
if (!verifySignature(signature, authUser.key)) {
|
|
return 401;
|
|
}
|
|
tickFetch();
|
|
return 200;
|
|
}
|
|
return 200;
|
|
}
|
|
export async function getSignatureUser(req) {
|
|
const signature = httpSignature.parseRequest(req, {
|
|
headers: []
|
|
});
|
|
const keyId = new URL(signature.keyId);
|
|
const dbResolver = new DbResolver();
|
|
// Retrieve from DB by HTTP-Signature keyId
|
|
const authUser = await dbResolver.getAuthUserFromKeyId(signature.keyId);
|
|
if (authUser) {
|
|
return authUser;
|
|
}
|
|
// Resolve if failed to retrieve by keyId
|
|
keyId.hash = "";
|
|
return await dbResolver.getAuthUserFromApId(getApId(keyId.toString()));
|
|
}
|
|
export function verifySignature(sig, key) {
|
|
if (![
|
|
'hs2019',
|
|
'rsa-sha256'
|
|
].includes(sig.algorithm.toLowerCase())) return false;
|
|
try {
|
|
return verify('rsa-sha256', Buffer.from(sig.signingString, 'utf8'), key.keyPem, Buffer.from(sig.params.signature, 'base64'));
|
|
} catch {
|
|
// Algo not supported
|
|
return false;
|
|
}
|
|
}
|
|
export function verifyDigest(body, digest) {
|
|
digest = toSingle(digest);
|
|
if (body == null || digest == null || !digest.toLowerCase().startsWith('sha-256=')) return false;
|
|
return createHash('sha256').update(body).digest('base64') === digest.substring(8);
|
|
}
|