Unverified Commit 0c9bc2e3 authored by Hong Minhee's avatar Hong Minhee
Browse files

skipSignatureVerification option

parent 39383e51
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -27,6 +27,7 @@ To be released.
        became `Temporal.DurationLike | false` (was `Temporal.DurationLike`).
     -  The type of `VerifyRequestOptions.timeWindow` property became
        `Temporal.DurationLike | false` (was `Temporal.DurationLike`).
     -  Added `CreateFederationOptions.skipSignatureVerification` property.

 -  Removed the singular actor key pair dispatcher APIs which were deprecated
    in version 0.10.0.
+129 −1
Original line number Diff line number Diff line
import { assert, assertEquals, assertFalse } from "@std/assert";
import { createRequestContext } from "../testing/context.ts";
import { mockDocumentLoader } from "../testing/docloader.ts";
import { rsaPublicKey2 } from "../testing/keys.ts";
import {
  rsaPrivateKey3,
  rsaPublicKey2,
  rsaPublicKey3,
} from "../testing/keys.ts";
import { test } from "../testing/mod.ts";
import {
  type Activity,
@@ -21,10 +25,13 @@ import {
  acceptsJsonLd,
  handleActor,
  handleCollection,
  handleInbox,
  handleObject,
  respondWithObject,
  respondWithObjectIfAcceptable,
} from "./handler.ts";
import { MemoryKvStore } from "./kv.ts";
import { signRequest } from "../sig/http.ts";

test("acceptsJsonLd()", () => {
  assert(acceptsJsonLd(
@@ -928,6 +935,127 @@ test("handleCollection()", async () => {
  assertEquals(onUnauthorizedCalled, null);
});

test("handleInbox()", async () => {
  const activity = new Create({
    id: new URL("https://example.com/activities/1"),
    actor: new URL("https://example.com/person2"),
    object: new Note({
      id: new URL("https://example.com/notes/1"),
      attribution: new URL("https://example.com/person2"),
      content: "Hello, world!",
    }),
  });
  const unsignedRequest = new Request("https://example.com/", {
    method: "POST",
    body: JSON.stringify(await activity.toJsonLd()),
  });
  const unsignedContext = createRequestContext({
    request: unsignedRequest,
    url: new URL(unsignedRequest.url),
    data: undefined,
  });
  let onNotFoundCalled: Request | null = null;
  const onNotFound = (request: Request) => {
    onNotFoundCalled = request;
    return new Response("Not found", { status: 404 });
  };
  const actorDispatcher: ActorDispatcher<void> = (_ctx, handle) => {
    if (handle !== "someone") return null;
    return new Person({ name: "Someone" });
  };
  const inboxOptions = {
    kv: new MemoryKvStore(),
    kvPrefixes: {
      activityIdempotence: ["_fedify", "activityIdempotence"],
      publicKey: ["_fedify", "publicKey"],
    },
    actorDispatcher,
    onNotFound,
    signatureTimeWindow: { minutes: 5 },
    skipSignatureVerification: false,
  } as const;
  let response = await handleInbox(unsignedRequest, {
    handle: null,
    context: unsignedContext,
    ...inboxOptions,
    actorDispatcher: undefined,
  });
  assertEquals(onNotFoundCalled, unsignedRequest);
  assertEquals(response.status, 404);

  onNotFoundCalled = null;
  response = await handleInbox(unsignedRequest, {
    handle: "nobody",
    context: unsignedContext,
    ...inboxOptions,
  });
  assertEquals(onNotFoundCalled, unsignedRequest);
  assertEquals(response.status, 404);

  onNotFoundCalled = null;
  response = await handleInbox(unsignedRequest, {
    handle: null,
    context: unsignedContext,
    ...inboxOptions,
  });
  assertEquals(onNotFoundCalled, null);
  assertEquals(response.status, 401);

  response = await handleInbox(unsignedRequest, {
    handle: "someone",
    context: unsignedContext,
    ...inboxOptions,
  });
  assertEquals(onNotFoundCalled, null);
  assertEquals(response.status, 401);

  onNotFoundCalled = null;
  const signedRequest = await signRequest(
    unsignedRequest.clone(),
    rsaPrivateKey3,
    rsaPublicKey3.id!,
  );
  const signedContext = createRequestContext({
    request: signedRequest,
    url: new URL(signedRequest.url),
    data: undefined,
    documentLoader: mockDocumentLoader,
  });
  response = await handleInbox(signedRequest, {
    handle: null,
    context: signedContext,
    ...inboxOptions,
  });
  assertEquals(onNotFoundCalled, null);
  assertEquals(response.status, 202);

  response = await handleInbox(signedRequest, {
    handle: "someone",
    context: signedContext,
    ...inboxOptions,
  });
  assertEquals(onNotFoundCalled, null);
  assertEquals(response.status, 202);

  response = await handleInbox(unsignedRequest, {
    handle: null,
    context: unsignedContext,
    ...inboxOptions,
    skipSignatureVerification: true,
  });
  assertEquals(onNotFoundCalled, null);
  assertEquals(response.status, 202);

  response = await handleInbox(unsignedRequest, {
    handle: "someone",
    context: unsignedContext,
    ...inboxOptions,
    skipSignatureVerification: true,
  });
  assertEquals(onNotFoundCalled, null);
  assertEquals(response.status, 202);
});

test("respondWithObject()", async () => {
  const response = await respondWithObject(
    new Note({
+20 −13
Original line number Diff line number Diff line
@@ -329,6 +329,7 @@ export interface InboxHandlerParameters<TContextData> {
  inboxErrorHandler?: InboxErrorHandler<TContextData>;
  onNotFound(request: Request): Response | Promise<Response>;
  signatureTimeWindow: Temporal.DurationLike | false;
  skipSignatureVerification: boolean;
}

export async function handleInbox<TContextData>(
@@ -344,6 +345,7 @@ export async function handleInbox<TContextData>(
    inboxErrorHandler,
    onNotFound,
    signatureTimeWindow,
    skipSignatureVerification,
  }: InboxHandlerParameters<TContextData>,
): Promise<Response> {
  const logger = getLogger(["fedify", "federation", "inbox"]);
@@ -422,6 +424,7 @@ export async function handleInbox<TContextData>(
  }
  let httpSigKey: CryptographicKey | null = null;
  if (activity == null) {
    if (!skipSignatureVerification) {
      const key = await verifyRequest(request, {
        contextLoader: context.contextLoader,
        documentLoader: context.documentLoader,
@@ -430,13 +433,17 @@ export async function handleInbox<TContextData>(
      });
      if (key == null) {
        logger.error("Failed to verify the request signature.", { handle });
      const response = new Response("Failed to verify the request signature.", {
        const response = new Response(
          "Failed to verify the request signature.",
          {
            status: 401,
            headers: { "Content-Type": "text/plain; charset=utf-8" },
      });
          },
        );
        return response;
      }
      httpSigKey = key;
    }
    activity = await Activity.fromJsonLd(json, context);
  }
  const cacheKey = activity.id == null
+12 −0
Original line number Diff line number Diff line
@@ -139,6 +139,15 @@ export interface CreateFederationOptions {
   */
  signatureTimeWindow?: Temporal.DurationLike | false;

  /**
   * Whether to skip HTTP Signatures verification for incoming activities.
   * This is useful for testing purposes, but should not be used in production.
   *
   * By default, this is `false` (i.e., signatures are verified).
   * @since 0.13.0
   */
  skipSignatureVerification?: boolean;

  /**
   * The retry policy for sending activities to recipients' inboxes.
   * By default, this uses an exponential backoff strategy with a maximum of
@@ -620,6 +629,7 @@ class FederationImpl<TContextData> implements Federation<TContextData> {
  authenticatedDocumentLoaderFactory: AuthenticatedDocumentLoaderFactory;
  onOutboxError?: OutboxErrorHandler;
  signatureTimeWindow: Temporal.DurationLike | false;
  skipSignatureVerification: boolean;
  outboxRetryPolicy: RetryPolicy;
  inboxRetryPolicy: RetryPolicy;

@@ -654,6 +664,7 @@ class FederationImpl<TContextData> implements Federation<TContextData> {
        getAuthenticatedDocumentLoader;
    this.onOutboxError = options.onOutboxError;
    this.signatureTimeWindow = options.signatureTimeWindow ?? { minutes: 1 };
    this.skipSignatureVerification = options.skipSignatureVerification ?? false;
    this.outboxRetryPolicy = options.outboxRetryPolicy ??
      createExponentialBackoffPolicy();
    this.inboxRetryPolicy = options.inboxRetryPolicy ??
@@ -1831,6 +1842,7 @@ class FederationImpl<TContextData> implements Federation<TContextData> {
          inboxErrorHandler: this.inboxErrorHandler,
          onNotFound,
          signatureTimeWindow: this.signatureTimeWindow,
          skipSignatureVerification: this.skipSignatureVerification,
        });
      case "following":
        return await handleCollection(request, {