Unverified Commit 3ad7569a authored by Hong Minhee's avatar Hong Minhee
Browse files

Instrument `signRequest()`

parent 6685aa78
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -66,6 +66,8 @@ To be released.
     -  Added `LookupObjectOptions.tracerProvider` option.
     -  Added `GetActorHandleOptions.tracerProvider` option.
     -  Added `VerifyRequestOptions.tracerProvider` option.
     -  Added `SignRequestOptions` interface.
     -  Added the optional fourth parameter to `signRequest()` function.

 -  Added `@fedify/fedify/x/sveltekit` module for integrating with [SvelteKit]
    hook.  [[#171], [#183] by Jiyu Park]
+24 −23
Original line number Diff line number Diff line
@@ -120,11 +120,12 @@ spans:
| Span name                      | [Span kind] | Description                           |
|--------------------------------|-------------|---------------------------------------|
| `{method} {template}`          | Server      | Serves the incoming HTTP request.     |
| `activitypub.lookup_object`    | Client      | Looks up the Activity Streams object. |
| `activitypub.get_actor_handle` | Client      | Resolves the actor handle.            |
| `webfinger.lookup`             | Client      | Looks up the WebFinger resource.      |
| `webfinger.handle`             | Server      | Handles the WebFinger request.        |
| `activitypub.lookup_object`    | Client      | Looks up the Activity Streams object. |
| `http_signatures.sign`         | Internal    | Signs the HTTP request.               |
| `http_signatures.verify`       | Internal    | Verifies the HTTP request signature.  |
| `webfinger.handle`             | Server      | Handles the WebFinger request.        |
| `webfinger.lookup`             | Client      | Looks up the WebFinger resource.      |

More operations will be instrumented in the future releases.

@@ -140,7 +141,7 @@ for ActivityPub as of November 2024. However, Fedify provides a set of semantic
for ActivityPub:

| Attribute                             | Type     | Description                                                                              | Example                                                              |
|-------------------------------------|----------|------------------------------------------------------------------------------------------|----------------------------------------------------------------------|
|---------------------------------------|----------|------------------------------------------------------------------------------------------|----------------------------------------------------------------------|
| `activitypub.activity.id`             | string   | The URI of the activity object.                                                          | `"https://example.com/activity/1"`                                   |
| `activitypub.activity.type`           | string[] | The qualified URI(s) of the activity type(s).                                            | `["https://www.w3.org/ns/activitystreams#Create"]`                   |
| `activitypub.activity.to`             | string[] | The URI(s) of the recipient collections/actors of the activity.                          | `["https://example.com/1/followers/2"]`                              |
@@ -153,10 +154,10 @@ for ActivityPub:
| `activitypub.object.in_reply_to`      | string[] | The URI(s) of the original object to which the object reply.                             | `["https://example.com/object/1"]`                                   |
| `activitypub.inboxes`                 | int      | The number of inboxes the activity is sent to.                                           | `12`                                                                 |
| `activitypub.shared_inbox`            | boolean  | Whether the activity is sent to the shared inbox.                                        | `true`                                                               |
| `httpsignatures.signature`          | string   | The signature of the HTTP request in hexadecimal.                                        | `"73a74c990beabe6e59cc68f9c6db7811b59cbb22fd12dcffb3565b651540efe9"` |
| `httpsignatures.algorithm`          | string   | The algorithm of the HTTP request signature.                                             | `"rsa-sha256"`                                                       |
| `httpsignatures.key_id`             | string   | The public key ID of the HTTP request signature.                                         | `"https://example.com/actor/1#main-key"`                             |
| `httpsignatures.digest.{algorithm}` | string   | The digest of the HTTP request body in hexadecimal.  The `{algorithm}` is the digest algorithm (e.g., `sha`, `sha-256`). | `"d41d8cd98f00b204e9800998ecf8427e"` |
| `http_signatures.signature`           | string   | The signature of the HTTP request in hexadecimal.                                        | `"73a74c990beabe6e59cc68f9c6db7811b59cbb22fd12dcffb3565b651540efe9"` |
| `http_signatures.algorithm`           | string   | The algorithm of the HTTP request signature.                                             | `"rsa-sha256"`                                                       |
| `http_signatures.key_id`              | string   | The public key ID of the HTTP request signature.                                         | `"https://example.com/actor/1#main-key"`                             |
| `http_signatures.digest.{algorithm}`  | string   | The digest of the HTTP request body in hexadecimal.  The `{algorithm}` is the digest algorithm (e.g., `sha`, `sha-256`). | `"d41d8cd98f00b204e9800998ecf8427e"` |
| `webfinger.resource`                  | string   | The queried resource URI.                                                                | `"acct:fedify@hollo.social"`                                         |
| `webfinger.resource.scheme`           | string   | The scheme of the queried resource URI.                                                  | `"acct"`                                                             |

+3 −0
Original line number Diff line number Diff line
@@ -535,6 +535,7 @@ export class FederationImpl<TContextData> implements Federation<TContextData> {
        activityId: message.activityId,
        inbox: new URL(message.inbox),
        headers: new Headers(message.headers),
        tracerProvider: this.tracerProvider,
      });
    } catch (error) {
      const activity = await Activity.fromJsonLd(message.activity, {
@@ -1844,6 +1845,7 @@ export class FederationImpl<TContextData> implements Federation<TContextData> {
                  inboxes[inbox],
                ),
            }),
            tracerProvider: this.tracerProvider,
          }),
        );
      }
@@ -3075,6 +3077,7 @@ export class InboxContextImpl<TContextData> extends ContextImpl<TContextData>
            activity: this.activity,
            activityId: activityId,
            inbox: new URL(inbox),
            tracerProvider: this.federation.tracerProvider,
          }),
        );
      }
+15 −1
Original line number Diff line number Diff line
import { getLogger } from "@logtape/logtape";
import { TracerProvider } from "@opentelemetry/api";
import { signRequest } from "../sig/http.ts";
import type { Recipient } from "../vocab/actor.ts";

@@ -102,6 +103,13 @@ export interface SendActivityParameters {
   * Additional headers to include in the request.
   */
  headers?: Headers;

  /**
   * The tracer provider for tracing the request.
   * If omitted, the global tracer provider is used.
   * @since 1.3.0
   */
  tracerProvider?: TracerProvider;
}

/**
@@ -118,6 +126,7 @@ export async function sendActivity(
    keys,
    inbox,
    headers,
    tracerProvider,
  }: SendActivityParameters,
): Promise<void> {
  const logger = getLogger(["fedify", "federation", "outbox"]);
@@ -150,7 +159,12 @@ export async function sendActivity(
      },
    );
  } else {
    request = await signRequest(request, rsaKey.privateKey, rsaKey.keyId);
    request = await signRequest(
      request,
      rsaKey.privateKey,
      rsaKey.keyId,
      { tracerProvider },
    );
  }
  let response: Response;
  try {
+82 −6
Original line number Diff line number Diff line
import { getLogger } from "@logtape/logtape";
import { type Span, trace, type TracerProvider } from "@opentelemetry/api";
import {
  type Span,
  SpanStatusCode,
  trace,
  type TracerProvider,
} from "@opentelemetry/api";
import {
  ATTR_HTTP_REQUEST_HEADER,
  ATTR_HTTP_REQUEST_METHOD,
@@ -13,6 +18,18 @@ import type { DocumentLoader } from "../runtime/docloader.ts";
import { CryptographicKey } from "../vocab/vocab.ts";
import { fetchKey, type KeyCache, validateCryptoKey } from "./key.ts";

/**
 * Options for {@link signRequest}.
 * @since 1.3.0
 */
export interface SignRequestOptions {
  /**
   * The OpenTelemetry tracer provider.  If omitted, the global tracer provider
   * is used.
   */
  tracerProvider?: TracerProvider;
}

/**
 * Signs a request using the given private key.
 * @param request The request to sign.
@@ -26,6 +43,50 @@ export async function signRequest(
  request: Request,
  privateKey: CryptoKey,
  keyId: URL,
  options: SignRequestOptions = {},
): Promise<Request> {
  const tracerProvider = options.tracerProvider ?? trace.getTracerProvider();
  const tracer = tracerProvider.getTracer(
    metadata.name,
    metadata.version,
  );
  return await tracer.startActiveSpan(
    "http_signatures.sign",
    async (span) => {
      try {
        const signed = await signRequestInternal(
          request,
          privateKey,
          keyId,
          span,
        );
        if (span.isRecording()) {
          span.setAttribute(ATTR_HTTP_REQUEST_METHOD, signed.method);
          span.setAttribute(ATTR_URL_FULL, signed.url);
          for (const [name, value] of signed.headers) {
            span.setAttribute(ATTR_HTTP_REQUEST_HEADER(name), value);
          }
          span.setAttribute("http_signatures.key_id", keyId.href);
        }
        return signed;
      } catch (error) {
        span.setStatus({
          code: SpanStatusCode.ERROR,
          message: String(error),
        });
        throw error;
      } finally {
        span.end();
      }
    },
  );
}

async function signRequestInternal(
  request: Request,
  privateKey: CryptoKey,
  keyId: URL,
  span: Span,
): Promise<Request> {
  validateCryptoKey(privateKey, "private");
  if (privateKey.algorithm.name !== "RSASSA-PKCS1-v1_5") {
@@ -43,6 +104,9 @@ export async function signRequest(
  if (!headers.has("Digest") && body != null) {
    const digest = await crypto.subtle.digest("SHA-256", body);
    headers.set("Digest", `SHA-256=${encodeBase64(digest)}`);
    if (span.isRecording()) {
      span.setAttribute("http_signatures.digest.sha-256", encodeHex(digest));
    }
  }
  if (!headers.has("Date")) {
    headers.set("Date", new Date().toUTCString());
@@ -64,6 +128,10 @@ export async function signRequest(
    headerNames.join(" ")
  }",signature="${encodeBase64(signature)}"`;
  headers.set("Signature", sigHeader);
  if (span.isRecording()) {
    span.setAttribute("http_signatures.algorithm", "rsa-sha256");
    span.setAttribute("http_signatures.signature", encodeHex(signature));
  }
  return new Request(request, {
    headers,
    body,
@@ -152,7 +220,15 @@ export async function verifyRequest(
        }
      }
      try {
        return await verifyRequestInternal(request, span, options);
        const key = await verifyRequestInternal(request, span, options);
        if (key == null) span.setStatus({ code: SpanStatusCode.ERROR });
        return key;
      } catch (error) {
        span.setStatus({
          code: SpanStatusCode.ERROR,
          message: String(error),
        });
        throw error;
      } finally {
        span.end();
      }
@@ -235,7 +311,7 @@ async function verifyRequestInternal(
        return null;
      }
      if (span.isRecording()) {
        span.setAttribute(`httpsignatures.digest.${algo}`, encodeHex(digest));
        span.setAttribute(`http_signatures.digest.${algo}`, encodeHex(digest));
      }
      const expectedDigest = await crypto.subtle.digest(
        supportedHashAlgorithms[algo],
@@ -311,10 +387,10 @@ async function verifyRequestInternal(
    return null;
  }
  const { keyId, headers, signature } = sigValues;
  span?.setAttribute("httpsignatures.key_id", keyId);
  span?.setAttribute("httpsignatures.signature", signature);
  span?.setAttribute("http_signatures.key_id", keyId);
  span?.setAttribute("http_signatures.signature", signature);
  if ("algorithm" in sigValues) {
    span?.setAttribute("httpsignatures.algorithm", sigValues.algorithm);
    span?.setAttribute("http_signatures.algorithm", sigValues.algorithm);
  }
  const { key, cached } = await fetchKey(new URL(keyId), CryptographicKey, {
    documentLoader,