Unverified Commit 83daea28 authored by Hong Minhee's avatar Hong Minhee
Browse files

Extract fetchKey() method

parent c4529cfa
Loading
Loading
Loading
Loading
+11 −1
Original line number Diff line number Diff line
@@ -83,6 +83,11 @@ To be released.
     -  `Object.clone()` method now accepts `proof` option.
     -  `Object.clone()` method now accepts `proofs` option.

 -  Implemented Object Integrity Proofs.  [[FEP-8b32], [#54]]

     -  Added `fetchKey()` function.
     -  Added `FetchKeyOptions` interface.

 -  Added `context` option to `Object.toJsonLd()` method.  This applies to
    any subclasses of the `Object` class too.

@@ -97,6 +102,11 @@ To be released.
        `following`, `followers`, `outbox`, `manuallyApprovesFollowers`, and
        `url`.

 -  Added more log messages using the [LogTape] library.  Currently the below
    logger categories are used:

     -  `["fedify", "sig", "key"]`

[#54]: https://github.com/dahlia/fedify/issues/54
[#55]: https://github.com/dahlia/fedify/issues/55
[FEP-521a]: https://codeberg.org/fediverse/fep/src/branch/main/fep/521a/fep-521a.md
+1 −1
Original line number Diff line number Diff line
@@ -102,7 +102,7 @@ export async function importMultibaseKey(key: string): Promise<CryptoKey> {
      ["verify"],
    );
  } else {
    throw new TypeError("Unsupported key type: " + code);
    throw new TypeError("Unsupported key type: 0x" + code.toString(16));
  }
}

+11 −73
Original line number Diff line number Diff line
import { getLogger } from "@logtape/logtape";
import { equals } from "@std/bytes";
import { decodeBase64, encodeBase64 } from "@std/encoding/base64";
import {
  type DocumentLoader,
  fetchDocumentLoader,
} from "../runtime/docloader.ts";
import { isActor } from "../vocab/actor.ts";
import { CryptographicKey, Object as ASObject } from "../vocab/vocab.ts";
import { validateCryptoKey } from "./key.ts";
import type { DocumentLoader } from "../runtime/docloader.ts";
import { CryptographicKey } from "../vocab/vocab.ts";
import { fetchKey, validateCryptoKey } from "./key.ts";

/**
 * Signs a request using the given private key.
@@ -24,6 +20,9 @@ export async function signRequest(
  keyId: URL,
): Promise<Request> {
  validateCryptoKey(privateKey, "private");
  if (privateKey.algorithm.name !== "RSASSA-PKCS1-v1_5") {
    throw new TypeError("Unsupported algorithm: " + privateKey.algorithm.name);
  }
  const url = new URL(request.url);
  const body: ArrayBuffer | null =
    request.method !== "GET" && request.method !== "HEAD"
@@ -226,72 +225,11 @@ export async function verifyRequest(
    return null;
  }
  const { keyId, headers, signature } = sigValues;
  logger.debug("Fetching key {keyId} to verify signature...", { keyId });
  let document: unknown;
  try {
    const remoteDocument = await (documentLoader ?? fetchDocumentLoader)(keyId);
    document = remoteDocument.document;
  } catch (_) {
    logger.debug("Failed to fetch key {keyId}.", { keyId });
    return null;
  }
  let object: ASObject | CryptographicKey;
  try {
    object = await ASObject.fromJsonLd(document, {
  const key = await fetchKey(new URL(keyId), CryptographicKey, {
    documentLoader,
    contextLoader,
  });
  } catch (e) {
    if (!(e instanceof TypeError)) throw e;
    try {
      object = await CryptographicKey.fromJsonLd(document, {
        documentLoader,
        contextLoader,
      });
    } catch (e) {
      if (e instanceof TypeError) {
        logger.debug(
          "Failed to verify; key {keyId} returned an invalid object.",
          { keyId },
        );
        return null;
      }
      throw e;
    }
  }
  let key: CryptographicKey | null = null;
  if (object instanceof CryptographicKey) key = object;
  else if (isActor(object)) {
    for await (
      const k of object.getPublicKeys({ documentLoader, contextLoader })
    ) {
      if (k.id?.href === keyId) {
        key = k;
        break;
      }
    }
    if (key == null) {
      logger.debug(
        "Failed to verify; object {keyId} returned an {actorType}, " +
          "but has no key matching {keyId}.",
        { keyId, actorType: object.constructor.name },
      );
      return null;
    }
  } else {
    logger.debug(
      "Failed to verify; key {keyId} returned an invalid object.",
      { keyId },
    );
    return null;
  }
  if (key.publicKey == null) {
    logger.debug(
      "Failed to verify; key {keyId} has no publicKeyPem field.",
      { keyId },
    );
    return null;
  }
  if (key == null) return null;
  const headerNames = headers.split(/\s+/g);
  if (
    !headerNames.includes("(request-target)") || !headerNames.includes("date")
+54 −1
Original line number Diff line number Diff line
import { assertEquals, assertRejects, assertThrows } from "@std/assert";
import { rsaPrivateKey2, rsaPublicKey2 } from "../testing/keys.ts";
import {
  ed25519Multikey,
  rsaPrivateKey2,
  rsaPublicKey1,
  rsaPublicKey2,
  rsaPublicKey3,
} from "../testing/keys.ts";
import { CryptographicKey, Multikey } from "../vocab/vocab.ts";
import {
  exportJwk,
  fetchKey,
  type FetchKeyOptions,
  generateCryptoKeyPair,
  importJwk,
  validateCryptoKey,
} from "./key.ts";
import { mockDocumentLoader } from "../testing/docloader.ts";

Deno.test("validateCryptoKey()", async () => {
  const pkcs1v15 = await crypto.subtle.generateKey(
@@ -186,3 +196,46 @@ Deno.test("importJwk()", async () => {
  assertRejects(() => importJwk(rsaPublicJwk, "private"));
  assertRejects(() => importJwk(rsaPrivateJwk, "public"));
});

Deno.test("fetchKey()", async () => {
  const options: FetchKeyOptions = {
    documentLoader: mockDocumentLoader,
    contextLoader: mockDocumentLoader,
  };
  assertEquals(
    await fetchKey("https://example.com/nothing", CryptographicKey, options),
    null,
  );
  assertEquals(
    await fetchKey("https://example.com/object", CryptographicKey, options),
    null,
  );
  assertEquals(
    await fetchKey("https://example.com/key", CryptographicKey, options),
    rsaPublicKey1,
  );
  assertEquals(
    await fetchKey(
      "https://example.com/person#no-key",
      CryptographicKey,
      options,
    ),
    null,
  );
  assertEquals(
    await fetchKey(
      "https://example.com/person2#key3",
      CryptographicKey,
      options,
    ),
    rsaPublicKey3,
  );
  assertEquals(
    await fetchKey(
      "https://example.com/person2#key4",
      Multikey,
      options,
    ),
    ed25519Multikey,
  );
});
+122 −0
Original line number Diff line number Diff line
import { getLogger } from "@logtape/logtape";
import {
  type DocumentLoader,
  fetchDocumentLoader,
} from "../runtime/docloader.ts";
import { isActor } from "../vocab/actor.ts";
import { CryptographicKey, type Multikey, Object } from "../vocab/vocab.ts";

/**
 * Checks if the given key is valid and supported.  No-op if the key is valid,
@@ -124,3 +130,119 @@ export async function importJwk(
  validateCryptoKey(key, type);
  return key;
}

/**
 * Options for {@link fetchKey}.
 * @since 0.10.0
 */
export interface FetchKeyOptions {
  /**
   * The document loader for loading remote JSON-LD documents.
   */
  documentLoader?: DocumentLoader;

  /**
   * The context loader for loading remote JSON-LD contexts.
   */
  contextLoader?: DocumentLoader;
}

/**
 * Fetches a {@link CryptographicKey} or {@link Multikey} from the given URL.
 * If the given URL contains an {@link Actor} object, it tries to find
 * the corresponding key in the `publicKey` or `assertionMethod` property.
 * @typeParam T The type of the key to fetch.  Either {@link CryptographicKey}
 *              or {@link Multikey}.
 * @param keyId The URL of the key.
 * @param cls The class of the key to fetch.  Either {@link CryptographicKey}
 *            or {@link Multikey}.
 * @param options Options for fetching the key.  See {@link FetchKeyOptions}.
 * @returns The fetched key or `null` if the key is not found.
 * @since 0.10.0
 */
export async function fetchKey<T extends CryptographicKey | Multikey>(
  keyId: URL | string,
  // deno-lint-ignore no-explicit-any
  cls: (new (...args: any[]) => T) & {
    fromJsonLd(
      jsonLd: unknown,
      options: {
        documentLoader?: DocumentLoader;
        contextLoader?: DocumentLoader;
      },
    ): Promise<T>;
  },
  { documentLoader, contextLoader }: FetchKeyOptions = {},
): Promise<T & { publicKey: CryptoKey } | null> {
  const logger = getLogger(["fedify", "sig", "key"]);
  keyId = typeof keyId === "string" ? keyId : keyId.href;
  logger.debug("Fetching key {keyId} to verify signature...", { keyId });
  let document: unknown;
  try {
    const remoteDocument = await (documentLoader ?? fetchDocumentLoader)(keyId);
    document = remoteDocument.document;
  } catch (_) {
    logger.debug("Failed to fetch key {keyId}.", { keyId });
    return null;
  }
  let object: Object | T;
  try {
    object = await Object.fromJsonLd(document, {
      documentLoader,
      contextLoader,
    });
  } catch (e) {
    if (!(e instanceof TypeError)) throw e;
    try {
      object = await cls.fromJsonLd(document, {
        documentLoader,
        contextLoader,
      });
    } catch (e) {
      if (e instanceof TypeError) {
        logger.debug(
          "Failed to verify; key {keyId} returned an invalid object.",
          { keyId },
        );
        return null;
      }
      throw e;
    }
  }
  let key: T | null = null;
  if (object instanceof cls) key = object;
  else if (isActor(object)) {
    // @ts-ignore: cls is either CryptographicKey or Multikey
    const keys = cls === CryptographicKey
      ? object.getPublicKeys({ documentLoader, contextLoader })
      : object.getAssertionMethods({ documentLoader, contextLoader });
    for await (const k of keys) {
      if (k.id?.href === keyId) {
        key = k as T;
        break;
      }
    }
    if (key == null) {
      logger.debug(
        "Failed to verify; object {keyId} returned an {actorType}, " +
          "but has no key matching {keyId}.",
        { keyId, actorType: object.constructor.name },
      );
      return null;
    }
  } else {
    logger.debug(
      "Failed to verify; key {keyId} returned an invalid object.",
      { keyId },
    );
    return null;
  }
  if (key.publicKey == null) {
    logger.debug(
      "Failed to verify; key {keyId} has no publicKeyPem field.",
      { keyId },
    );
    return null;
  }
  return key as T & { publicKey: CryptoKey };
}
Loading