Skip to content

OpenTelemetry: metrics for signature verification duration#769

Open
dahlia wants to merge 12 commits into
fedify-dev:mainfrom
dahlia:otel/sig-metrics
Open

OpenTelemetry: metrics for signature verification duration#769
dahlia wants to merge 12 commits into
fedify-dev:mainfrom
dahlia:otel/sig-metrics

Conversation

@dahlia
Copy link
Copy Markdown
Member

@dahlia dahlia commented May 15, 2026

Resolves #737, a sub-issue of #316.

Summary

Fedify already wraps each signature verification in a span (http_signatures.verify, ld_signatures.verify, object_integrity_proofs.verify), but spans answer individual-request questions. Operators couldn't easily compute p95 verification latency, see whether key-fetch failures were inflating inbox latency, or compare OIP verification cost to HTTP Signature cost. PR #755 (issue #619) already shipped the meterProvider option and the FederationMetrics scaffolding for histograms; this PR fills in the verification-side instrumentation.

activitypub.signature.verification.duration (unit ms) records the time the verifier spent end to end, including local key lookup and any remote key fetch. It carries activitypub.signature.kind (http, linked_data, or object_integrity) and activitypub.signature.result (verified, rejected, missing, or error). OIP never produces missing, because the caller decides whether to invoke proof verification at all. Spec-bounded optional attributes are recorded only when the parsed value matches a small whitelist, so attacker-supplied JSON-LD or signature headers cannot inflate cardinality: http_signatures.algorithm (draft-cavage names plus the RFC 9421 algorithm map), ld_signatures.type (RsaSignature2017), object_integrity_proofs.cryptosuite (eddsa-jcs-2022), and http_signatures.failure_reason on rejected HTTP rows (invalidSignature or keyFetchError).

activitypub.signature.key_fetch.duration (unit ms) measures the public key lookup separately, so operators can subtract fetch time from the total to isolate the rest of the verification work: canonicalization, hashing, attribution and owner checks, cryptographic verification. Each fetch attempt is its own measurement; a verification that retries after a cache mismatch emits two key-fetch measurements alongside the single verification measurement that covers both. The result attribute is hit (served by the configured KeyCache), fetched (loaded through the document loader with a usable key), or error.

The new option is optional and defaults to the global MeterProvider, so existing callers see no behavior change beyond the new metric emissions.

Implementation notes

The three verify-options interfaces in packages/fedify/src/sig/http.ts, packages/fedify/src/sig/ld.ts, and packages/fedify/src/sig/proof.ts now accept an optional meterProvider. The option is threaded from the existing call sites in packages/fedify/src/federation/handler.ts and packages/fedify/src/federation/middleware.ts. The HTTP double-knock recursive calls also pick up the tracerProvider they had been silently dropping, alongside the new meterProvider.

Avoiding double-counting required two small refactors. The cached-key retry inside verifyRequestDraft and verifyRequestRfc9421 now calls the inner spec helper directly, reusing the outer span and a new mutable metricsContext, so each public verifyRequestDetailed call emits exactly one span and one verification measurement. The OIP cached-key retry inside verifyProofInternal does the same. The RFC 9421 multi-signature loop uses a setFailure(result, algorithm?) helper so an earlier candidate's bounded algorithm cannot stick to a later candidate's failure.

The key-fetch instrumentation lives in per-file measureKeyFetch/measureLdKeyFetch/measureOipKeyFetch helpers. They start performance.now() immediately before invoking the fetch closure and record the result as soon as the promise settles, so the canonicalization and SHA-256 work running concurrently in the OIP path does not pollute the measurement. Throws are recorded as error and rethrown, so verifier behavior is unchanged.

Documentation in docs/manual/opentelemetry.md now lists both histograms in the Instrumented metrics table and documents the full attribute set, including the per-call vs per-fetch granularity and the exact spec-bounded value lists. CHANGES.md has a 2.3.0 bullet under @fedify/fedify with [#316] and [#737] references.

Test plan

  • mise run check
  • mise run test:deno (31,505 tests, 447 sub-steps)
  • mise run test:node
  • mise run docs:build
  • New tests in packages/fedify/src/sig/http.test.ts, packages/fedify/src/sig/ld.test.ts, and packages/fedify/src/sig/proof.test.ts cover the verified, rejected, missing, error, cardinality, no-double-count, and cache-hit vs fetched paths for each signature kind.

dahlia added 9 commits May 16, 2026 00:41
Add an optional meterProvider?: MeterProvider field to the three
OpenTelemetry-aware verifier option types, mirroring the existing
tracerProvider:

  - VerifyRequestOptions in sig/http.ts
  - VerifySignatureOptions in sig/ld.ts (inherited by
    VerifyJsonLdOptions)
  - VerifyProofOptions in sig/proof.ts (inherited by
    VerifyObjectOptions)

Thread the option from the existing inbox/outbox call sites:

  - handler.ts's handleInboxInternal forwards parameters.meterProvider
    to verifyJsonLd, verifyObject, and verifyRequestDetailed.
  - middleware.ts's InboxContext.handleObject and getSignedKey forward
    this.meterProvider.

verifyRequestDraft and verifyRequestRfc9421 now also destructure
meterProvider, and the recursive verifyRequestDetailed calls inside
the HTTP double-knock fallback thread both meterProvider and
tracerProvider through (previously they silently fell back to the
global tracer provider on retry, which would have split spans across
providers; this fixes that drop alongside the new meterProvider).

No metrics are recorded yet. Subsequent commits will add the
activitypub.signature.verification.duration and
activitypub.signature.key_fetch.duration histograms that consume this
option.

fedify-dev#316
fedify-dev#737

Assisted-by: Claude Code:claude-opus-4-7
Add the `activitypub.signature.verification.duration` OpenTelemetry
histogram (unit `ms`) and instrument the HTTP Signatures verification
path so each public call to `verifyRequestDetailed()` (and its
`verifyRequest()` wrapper) emits exactly one measurement.

Each measurement carries:

  - `activitypub.signature.kind` = `http`
  - `activitypub.signature.result` = `verified`, `rejected`, `missing`,
    or `error`
  - `http_signatures.algorithm`, only when the parsed algorithm value
    comes from a bounded set.  For draft-cavage signatures the value
    is gated by a `DRAFT_KNOWN_ALGORITHMS` whitelist; for RFC 9421 it
    is recorded only after the algorithm matches
    `rfc9421AlgorithmMap`.  Both gates keep the metric attribute on a
    small spec-bounded set so attacker-supplied algorithm strings
    cannot inflate cardinality.
  - `http_signatures.failure_reason`, only on `rejected` rows, taken
    from the `VerifyRequestFailureReason.type` discriminator.

The new `FederationMetrics.recordSignatureVerificationDuration()`
helper writes the measurement through the existing
`getFederationMetrics(meterProvider)` cache, mirroring the shape of
the existing delivery and queue-task metric helpers.

Implementation notes:

  - `verifyRequestDraft()` and `verifyRequestRfc9421()` now accept a
    mutable `HttpSignatureMetricsContext` so the outer
    `verifyRequestDetailed()` can read back the bounded algorithm of
    the candidate whose result was actually returned, without
    surfacing it on the public result type.
  - `verifyRequestRfc9421()` tracks the bounded algorithm per
    candidate via a `setFailure(result, algorithm?)` helper, so a
    later candidate cannot inherit an earlier candidate's algorithm
    when iterating multi-signature inputs.
  - The cached-key retry paths in both spec implementations now call
    `verifyRequestDraft()` / `verifyRequestRfc9421()` directly with
    the outer span and metrics context, instead of recursing through
    `verifyRequestDetailed()`.  This collapses the retry into a
    single observed verification (one `http_signatures.verify` span
    and one histogram measurement per public call), and is regression-
    guarded by a new `cached-key retry emits one measurement, not
    two` test step.

New tests cover the verified, missing, invalid-signature, key-fetch
failure, double-count (cached-key retry and `verifyRequest()`
wrapper), and unknown-algorithm cardinality paths.

fedify-dev#316
fedify-dev#737

Assisted-by: Claude Code:claude-opus-4-7
Assisted-by: Codex:gpt-5.5
Extend the `activitypub.signature.verification.duration` histogram to
cover Linked Data Signatures.  `verifyJsonLd()` now emits exactly one
measurement per call, carrying:

  - `activitypub.signature.kind` = `linked_data`
  - `activitypub.signature.result` = `verified`, `rejected`, `missing`,
    or `error`
  - `ld_signatures.type`, gated by a new `LD_KNOWN_SIGNATURE_TYPES`
    whitelist (currently `RsaSignature2017`).  Unknown type strings,
    including attacker-supplied ones, keep their span attribute but are
    dropped from the metric so cardinality stays bounded.

Result classification:

  - thrown error → `error`
  - returned `true` → `verified`
  - returned `false` with a `signature` property present (even when it
    is `null` or otherwise malformed) → `rejected`
  - returned `false` with no `signature` property at all → `missing`

Two small helpers tidy up the existing inline checks:

  - `hasLdSignatureProperty()` does the loose presence check used for
    classification, so malformed signatures are still reported as
    `rejected` rather than `missing`.
  - `getLdSignatureObject()` extracts the inner signature record only
    when it is a plain object, driving both the existing span
    attributes and the new bounded-type lookup.

New test cases in *packages/fedify/src/sig/ld.test.ts* cover verified,
missing, invalid, malformed-null-signature, and unknown-type cardinality
paths.

fedify-dev#316
fedify-dev#737

Assisted-by: Claude Code:claude-opus-4-7
Assisted-by: Codex:gpt-5.5
Extend the `activitypub.signature.verification.duration` histogram to
cover Object Integrity Proofs.  `verifyProof()` now emits exactly one
measurement per public call, carrying:

  - `activitypub.signature.kind` = `object_integrity`
  - `activitypub.signature.result` = `verified`, `rejected`, or
    `error`.  OIP intentionally has no `missing` value, because
    `verifyObject()` decides whether to call `verifyProof()` at all.
  - `object_integrity_proofs.cryptosuite`, gated by a new
    `OIP_KNOWN_CRYPTOSUITES` whitelist (currently `eddsa-jcs-2022`).
    Unknown cryptosuite strings keep their span attribute but are
    dropped from the metric so cardinality stays bounded.

Result classification:

  - thrown error → `error`
  - `verifyProofInternal()` returns a non-null Multikey → `verified`
  - `verifyProofInternal()` returns null → `rejected`

The two cached-key retry paths inside `verifyProofInternal()` now
recurse via `verifyProofInternal()` instead of `verifyProof()`, so the
retry stays inside the outer `verifyProof()`'s span and timing context.
Each public `verifyProof()` call therefore emits exactly one
`object_integrity_proofs.verify` span and one duration measurement.

New tests in *packages/fedify/src/sig/proof.test.ts* cover verified,
rejected, cached-key retry no-double-count, and `verifyObject()`
wrapper no-double-count paths.

fedify-dev#316
fedify-dev#737

Assisted-by: Claude Code:claude-opus-4-7
Assisted-by: Codex:gpt-5.5
Add the `activitypub.signature.key_fetch.duration` OpenTelemetry
histogram (unit `ms`), recorded around every public key lookup that
runs inside one of Fedify's three signature verification paths.  The
new metric lets operators subtract key fetch time from total
verification time to isolate crypto-only latency.

Each measurement carries:

  - `activitypub.signature.kind`: `http`, `linked_data`, or
    `object_integrity`
  - `activitypub.signature.key_fetch.result`: `hit` (served from the
    in-process key cache), `fetched` (network roundtrip returned a
    usable key), or `error` (no usable key, including throws)

The metric is recorded one-per-fetch-attempt, deliberately distinct
from `activitypub.signature.verification.duration`, which is
one-per-public-call.  A verification that retries after a cache
mismatch therefore emits two key fetch measurements (one `hit` for the
stale attempt, one `fetched` for the freshly fetched retry) plus a
single verification measurement that covers both.

Each verifier wraps its `fetchKey` / `fetchKeyDetailed` calls in a
small per-file helper (`measureKeyFetch` in http.ts, `measureLdKeyFetch`
in ld.ts, `measureOipKeyFetch` in proof.ts) so:

  - the timer starts immediately before the fetch and ends as soon as
    the promise settles, even when concurrent local work (JCS
    canonicalization, SHA-256 digest, etc.) runs alongside the fetch;
  - thrown errors are recorded as `error` and rethrown, so verifier
    behavior is unchanged;
  - no key IDs, URLs, hostnames, or other unbounded values reach the
    metric attributes.

Tests cover `hit`, `fetched`, and `error` paths for HTTP, plus
`fetched` for LD and OIP, plus `hit` for OIP, plus a negative test
asserting that an unsigned document under `verifyJsonLd()` emits no
key fetch measurement at all.

fedify-dev#316
fedify-dev#737

Assisted-by: Claude Code:claude-opus-4-7
Assisted-by: Codex:gpt-5.5
Extend docs/manual/opentelemetry.md to cover the two new histograms:
activitypub.signature.verification.duration and
activitypub.signature.key_fetch.duration.

The metric attribute reference now explains:

  - that activitypub.signature.kind takes one of http, linked_data, or
    object_integrity, and activitypub.signature.result takes one of
    verified, rejected, missing, or error (with a note that
    object_integrity never produces missing);
  - that the verification.duration histogram covers the full
    verification path including local key lookup and remote key fetches,
    so operators can subtract key_fetch.duration to isolate the
    non-fetch verification work (canonicalization, hashing, attribution
    and owner checks, cryptographic verification);
  - that direct calls to verifyRequest() / verifyRequestDetailed(),
    verifyJsonLd(), and verifyProof() emit exactly one measurement per
    public call, while wrappers such as verifyObject() emit one
    measurement per inner verifyProof() call;
  - that http_signatures.algorithm, ld_signatures.type, and
    object_integrity_proofs.cryptosuite are recorded only when the
    parsed value matches a small spec-bounded set, listing the exact
    set for each kind;
  - that http_signatures.failure_reason on rejected rows is one of
    invalidSignature or keyFetchError, and that missing HTTP
    signatures use activitypub.signature.result=missing without a
    failure_reason attribute;
  - that key IDs, actor IDs, URLs, and object IDs are deliberately
    excluded from both histograms;
  - that key_fetch.duration is recorded per fetch attempt, so a
    cache-mismatch retry emits two key fetch measurements alongside the
    single verification measurement that covers both;
  - that hit corresponds to a result served by the configured KeyCache
    (which may itself be backed by Redis or a database), and fetched
    corresponds to a key loaded through the document loader, regardless
    of whether the document loader hits the network or a local store.

fedify-dev#316
fedify-dev#737

Assisted-by: Claude Code:claude-opus-4-7
Assisted-by: Codex:gpt-5.5
Add a 2.3.0 changelog entry for the new
activitypub.signature.verification.duration and
activitypub.signature.key_fetch.duration histograms, listing the
always-present and bounded conditional attributes, and the
corresponding issue link reference.

fedify-dev#316
fedify-dev#737

Assisted-by: Claude Code:claude-opus-4-7
The exported type's JSDoc still described `hit` as "served from the
in-process key cache" and `fetched` as "required a network round trip,"
which no longer matches the implementation or
docs/manual/opentelemetry.md.  `KeyCache` can be backed by a remote
store such as Redis or a database, and `fetched` only requires loading
through the document loader (typically but not exclusively a network
roundtrip).

Update the JSDoc to mirror the manual.

fedify-dev#316
fedify-dev#737

Assisted-by: Codex:gpt-5.5
@dahlia dahlia added this to the Fedify 2.3 milestone May 15, 2026
@dahlia dahlia self-assigned this May 15, 2026
@dahlia dahlia added type/enhancement Improvements to existing features component/federation Federation object related component/signatures OIP or HTTP/LD Signatures related component/otel OpenTelemetry integration labels May 15, 2026
@dahlia
Copy link
Copy Markdown
Member Author

dahlia commented May 15, 2026

@codex review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 15, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds OpenTelemetry instrumentation for signature verification and public key fetch duration across three signature types (HTTP Signatures, Linked Data Signatures, Object Integrity Proofs) with bounded cardinality attributes, threads meterProvider through verification call paths, adds histogram recording helpers, tests, and documentation.

Changes

Signature Verification Metrics Instrumentation

Layer / File(s) Summary
Metric type contract and recording methods
packages/fedify/src/federation/metrics.ts
New exported types SignatureVerificationKind, SignatureVerificationResult, SignatureKeyFetchResult, and SignatureVerificationExtraAttributes. Two Histogram instruments (activitypub.signature.verification.duration and activitypub.signature.key_fetch.duration) added to FederationMetrics with recording methods emitting bounded attributes.
HTTP signature verification instrumentation
packages/fedify/src/sig/http.ts
verifyRequestDetailed now initializes metrics state and records verification-duration in finally. Introduces HttpSignatureMetricsContext, measureKeyFetch() to record single key-fetch-duration per attempt, bounded DRAFT_KNOWN_ALGORITHMS, and classification helpers. Draft and RFC 9421 paths reuse outer span/metricsContext and thread meterProvider to avoid double-measurement and retain consistent algorithm/failure attribution.
HTTP signature metrics tests
packages/fedify/src/sig/http.test.ts
Adds tests using createTestMeterProvider() and assertGreaterOrEqual to assert histogram measurements and attributes across verified, missing, invalid, key-fetch error, cache/retry, and unknown-algorithm scenarios.
Linked Data signature verification instrumentation
packages/fedify/src/sig/ld.ts
Adds meter imports, measureLdKeyFetch wrapper for timing fetchKey, threads meterProvider through VerifySignatureOptions, bounds LD_KNOWN_SIGNATURE_TYPES, and refactors verifyJsonLd to classify outcomes and record verification/key-fetch durations with bounded ldType.
Linked Data signature metrics tests
packages/fedify/src/sig/ld.test.ts
Adds verifyJsonLd() tests using a test meter provider to validate verification and key-fetch histogram emissions for verified/missing/rejected/malformed, cold-cache, cached-key retry, and unknown-type cases.
Object Integrity Proof verification instrumentation
packages/fedify/src/sig/proof.ts
Adds meter imports, OIP_KNOWN_CRYPTOSUITES, measureOipKeyFetch, extends VerifyProofOptions with meterProvider, records verification-duration in finally with bounded cryptosuite, and changes retries to recurse into verifyProofInternal() to reuse the outer span/measurement.
Object Integrity Proof metrics tests
packages/fedify/src/sig/proof.test.ts
Adds tests using createTestMeterProvider() asserting activitypub.signature.verification.duration and activitypub.signature.key_fetch.duration emissions, classification, cached-key retry behavior, and cryptosuite attribute omission when unknown.
Metrics provider wiring
packages/fedify/src/federation/handler.ts, packages/fedify/src/federation/middleware.ts
handleInboxInternal now extracts meterProvider and forwards it into verifyJsonLd, verifyObject, and verifyRequestDetailed. Middleware verification calls also thread meterProvider.
Documentation and changelog
docs/manual/opentelemetry.md, CHANGES.md
Documents activitypub.signature.verification.duration and activitypub.signature.key_fetch.duration, their required/optional bounded attributes and result buckets (verified, rejected, missing, error; hit/fetched/error), retry/key-fetch measurement semantics, and adds Version 2.3.0 changelog entry.

Sequence Diagram

sequenceDiagram
  participant verifyRequestDetailed
  participant metricsContext as MetricsContext
  participant draft_or_rfc as SpecVerifier
  participant measureKeyFetch
  participant FederationMetrics

  verifyRequestDetailed->>metricsContext: init timing state (start, algorithm)
  verifyRequestDetailed->>draft_or_rfc: call with metricsContext & meterProvider
  draft_or_rfc->>measureKeyFetch: wrap fetchKeyDetailed
  measureKeyFetch->>FederationMetrics: recordSignatureKeyFetchDuration(kind, result)
  draft_or_rfc->>verifyRequestDetailed: return result
  verifyRequestDetailed->>FederationMetrics: recordSignatureVerificationDuration(kind, result, algorithm/failure)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

  • #737: Implements the requested OpenTelemetry histogram instruments and bounded attributes for signature verification duration and key fetch duration.

Possibly related PRs

  • fedify-dev/fedify#710: Overlaps on proof verification retry/verification code paths modified here.
  • fedify-dev/fedify#755: Related work threading meterProvider through federation handler and emitting signature verification/inbox metrics.

Suggested reviewers

  • 2chanhaeng
  • sij411
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'OpenTelemetry: metrics for signature verification duration' directly summarizes the main change: adding OpenTelemetry metrics for signature verification duration.
Description check ✅ Passed The description is comprehensive and clearly related to the changeset, explaining the motivation, implementation details, attribute design, and test coverage for the new metrics.
Linked Issues check ✅ Passed The PR fully implements the requirements from issue #737: adds activitypub.signature.verification.duration and activitypub.signature.key_fetch.duration histograms with proper attributes (kind, result, bounded optional attributes), avoids double-counting, includes documentation updates, and adds comprehensive tests for verified/rejected/missing/error paths and cache scenarios.
Out of Scope Changes check ✅ Passed All changes are in-scope: metric instrumentation for signature verification (http.ts, ld.ts, proof.ts), thread meterProvider through handler/middleware, documentation updates, and comprehensive tests. No unrelated refactoring or feature additions outside the stated objectives.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces two new OpenTelemetry histograms, activitypub.signature.verification.duration and activitypub.signature.key_fetch.duration, to monitor the performance of ActivityPub signature verification and public key lookups. The changes include updates to the core federation metrics, documentation, and verification logic for HTTP Signatures, Linked Data Signatures, and Object Integrity Proofs. Review feedback suggests refactoring the measureOipKeyFetch helper to use async/await for consistency with other helpers in the PR and adding a null check for verificationMethodId in the proof verification logic to improve robustness against malformed data.

Comment thread packages/fedify/src/sig/proof.ts Outdated
Comment thread packages/fedify/src/sig/proof.ts
@chatgpt-codex-connector
Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 🎉

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/fedify/src/sig/ld.test.ts`:
- Around line 346-527: Add a new test step to verify the cached-key retry path:
call verifyJsonLd with a custom keyCache whose get returns a valid but "stale"
CryptographicKey (and set is a noop) so the retry path in verifyJsonLd is hit;
use createTestMeterProvider to capture metrics, assert the overall verification
result, then call
recorder.getMeasurements("activitypub.signature.key_fetch.duration") and assert
there are exactly 2 measurements with the first metric having attribute
"activitypub.signature.key_fetch.result" === "hit" and the second === "fetched";
reference verifyJsonLd, keyCache.get/keyCache.set, CryptographicKey and
generateCryptoKeyPair to construct the cached key and recorder.getMeasurements
to assert the two entries.

In `@packages/fedify/src/sig/proof.test.ts`:
- Around line 571-793: Add a test step to assert that an unrecognized
cryptosuite is omitted from metric attributes: create a cloned proof with
cryptosuite "ecdsa-rdfc-2019" (use proof.clone or construct a DataIntegrityProof
with that cryptosuite), call verifyProof(jsonLd, unknownProof, { documentLoader:
mockDocumentLoader, contextLoader: mockDocumentLoader, meterProvider }) using
createTestMeterProvider, then check the
"activitypub.signature.verification.duration" measurement has result "rejected"
and that "object_integrity_proofs.cryptosuite" is not present in
measurements[0].attributes; this locks the bounding behavior tied to
OIP_KNOWN_CRYPTOSUITES.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: fdf94651-712f-4968-ab5d-11a9aa1ce215

📥 Commits

Reviewing files that changed from the base of the PR and between fafcfa7 and 99a15a3.

📒 Files selected for processing (11)
  • CHANGES.md
  • docs/manual/opentelemetry.md
  • packages/fedify/src/federation/handler.ts
  • packages/fedify/src/federation/metrics.ts
  • packages/fedify/src/federation/middleware.ts
  • packages/fedify/src/sig/http.test.ts
  • packages/fedify/src/sig/http.ts
  • packages/fedify/src/sig/ld.test.ts
  • packages/fedify/src/sig/ld.ts
  • packages/fedify/src/sig/proof.test.ts
  • packages/fedify/src/sig/proof.ts

Comment thread packages/fedify/src/sig/ld.test.ts
Comment thread packages/fedify/src/sig/proof.test.ts
@codecov
Copy link
Copy Markdown

codecov Bot commented May 15, 2026

Codecov Report

❌ Patch coverage is 87.13450% with 44 lines in your changes missing coverage. Please review.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
packages/fedify/src/sig/http.ts 81.88% 22 Missing and 3 partials ⚠️
packages/fedify/src/sig/proof.ts 80.35% 8 Missing and 3 partials ⚠️
packages/fedify/src/sig/ld.ts 91.11% 6 Missing and 2 partials ⚠️
Files with missing lines Coverage Δ
packages/fedify/src/federation/handler.ts 79.72% <100.00%> (+0.06%) ⬆️
packages/fedify/src/federation/metrics.ts 99.71% <100.00%> (+0.05%) ⬆️
packages/fedify/src/federation/middleware.ts 96.21% <100.00%> (+<0.01%) ⬆️
packages/fedify/src/sig/ld.ts 88.98% <91.11%> (-0.46%) ⬇️
packages/fedify/src/sig/proof.ts 76.10% <80.35%> (+0.50%) ⬆️
packages/fedify/src/sig/http.ts 79.65% <81.88%> (+0.47%) ⬆️

... and 1 file with indirect coverage changes

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

dahlia added 3 commits May 16, 2026 04:58
The OIP key-fetch timing helper used `Promise.prototype.then()` with
success and error handlers, while the matching helpers in *http.ts*
(`measureKeyFetch`) and *ld.ts* (`measureLdKeyFetch`) use async/await
with try/catch.  Rewriting `measureOipKeyFetch` the same way removes
that local inconsistency.

The function is still invoked synchronously up to its first `await`,
so `verifyProofInternal()`'s concurrent overlap of the key fetch with
JCS canonicalization and SHA-256 digest work is preserved.

fedify-dev#769 (comment)

Assisted-by: Claude Code:claude-opus-4-7
The LD path (`verifySignature()`) retries with a fresh fetch when the
cached key fails to verify, and the surrounding documentation states
that each retry emits its own `activitypub.signature.key_fetch.duration`
measurement.  HTTP and OIP already had regression tests for this; LD
did not.

Add a step that primes the keyCache with a wrong-but-valid RSA key for
the test vector's signer, runs `verifyJsonLd()`, and asserts the two
key-fetch measurements appear in order: `result=hit` for the stale
cached attempt, then `result=fetched` for the fresh retry that finally
verifies.

fedify-dev#769 (comment)

Assisted-by: Claude Code:claude-opus-4-7
Add a regression test that verifies an unrecognized cryptosuite does
not flow through to the `object_integrity_proofs.cryptosuite` metric
attribute.  `DataIntegrityProof`'s constructor and `clone()` reject
any cryptosuite other than `eddsa-jcs-2022`, so the exotic proof is
built through `DataIntegrityProof.fromJsonLd()`, which is permissive
enough to accept arbitrary field values.

`verifyProof()` then records `result=rejected` (the internal validator
returns null for any cryptosuite other than `eddsa-jcs-2022`), and the
test asserts the histogram measurement does not carry an
`object_integrity_proofs.cryptosuite` attribute.  This locks in the
`OIP_KNOWN_CRYPTOSUITES` whitelist as a defense against
attacker-controlled cryptosuite strings inflating metric cardinality.

fedify-dev#769 (comment)

Assisted-by: Claude Code:claude-opus-4-7
@dahlia
Copy link
Copy Markdown
Member Author

dahlia commented May 15, 2026

/gemini review

@dahlia
Copy link
Copy Markdown
Member Author

dahlia commented May 15, 2026

@codex review

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/fedify/src/sig/proof.test.ts`:
- Around line 663-693: Add assertions that the key-fetch sequence produced two
measurements: call
recorder.getMeasurements("activitypub.signature.key_fetch.duration"), assert its
length is 2, and assert the first measurement's attributes indicate the cache
"hit" and the second indicates "fetched" (e.g.,
measurements[0].attributes.result === "hit" and
measurements[1].attributes.result === "fetched") so the verifyProof retry path
(verifyProof / proof.ts logic) is validated to emit hit then fetched.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 5aca9a40-b389-4d98-960c-379b5ddca482

📥 Commits

Reviewing files that changed from the base of the PR and between 99a15a3 and b3d2090.

📒 Files selected for processing (3)
  • packages/fedify/src/sig/ld.test.ts
  • packages/fedify/src/sig/proof.test.ts
  • packages/fedify/src/sig/proof.ts

Comment on lines +663 to +693
await t.step("cached-key retry emits one measurement, not two", async () => {
const [meterProvider, recorder] = createTestMeterProvider();
const keyId = "https://server.example/users/alice#ed25519-key";
// Prime the cache with a wrong-algorithm key (the rsaPublicKey2 is RSA,
// not Ed25519) so that verifyProofInternal falls through to the
// fresh-fetch retry path.
const cache: Record<string, CryptographicKey | Multikey | null> = {
[keyId]: rsaPublicKey2,
};
const key = await verifyProof(jsonLd, proof, {
documentLoader: mockDocumentLoader,
contextLoader: mockDocumentLoader,
meterProvider,
keyCache: {
get(id) {
return Promise.resolve(cache[id.href]);
},
set(id, k) {
cache[id.href] = k;
return Promise.resolve();
},
} satisfies KeyCache,
});
assert(key != null);
assertEquals(
recorder.getMeasurements(
"activitypub.signature.verification.duration",
).length,
1,
);
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Strengthen the cached-retry test by asserting key-fetch sequence.

This step already validates the no-double-count contract for activitypub.signature.verification.duration, but it leaves the companion key-fetch behavior for the same path unchecked. Per the PR objective ("a verification that retries emits multiple key-fetch measurements and a single verification measurement"), the recursion at proof.ts Lines 505-515 should produce two activitypub.signature.key_fetch.duration measurements in order: hit (the primed RSA key) followed by fetched (the forced refetch). This mirrors the LD cached-key retry test added in this PR.

🧪 Proposed additional assertions
     assert(key != null);
     assertEquals(
       recorder.getMeasurements(
         "activitypub.signature.verification.duration",
       ).length,
       1,
     );
+    const keyFetches = recorder.getMeasurements(
+      "activitypub.signature.key_fetch.duration",
+    );
+    assertEquals(keyFetches.length, 2);
+    assertEquals(
+      keyFetches[0].attributes["activitypub.signature.key_fetch.result"],
+      "hit",
+    );
+    assertEquals(
+      keyFetches[1].attributes["activitypub.signature.key_fetch.result"],
+      "fetched",
+    );
   });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
await t.step("cached-key retry emits one measurement, not two", async () => {
const [meterProvider, recorder] = createTestMeterProvider();
const keyId = "https://server.example/users/alice#ed25519-key";
// Prime the cache with a wrong-algorithm key (the rsaPublicKey2 is RSA,
// not Ed25519) so that verifyProofInternal falls through to the
// fresh-fetch retry path.
const cache: Record<string, CryptographicKey | Multikey | null> = {
[keyId]: rsaPublicKey2,
};
const key = await verifyProof(jsonLd, proof, {
documentLoader: mockDocumentLoader,
contextLoader: mockDocumentLoader,
meterProvider,
keyCache: {
get(id) {
return Promise.resolve(cache[id.href]);
},
set(id, k) {
cache[id.href] = k;
return Promise.resolve();
},
} satisfies KeyCache,
});
assert(key != null);
assertEquals(
recorder.getMeasurements(
"activitypub.signature.verification.duration",
).length,
1,
);
});
await t.step("cached-key retry emits one measurement, not two", async () => {
const [meterProvider, recorder] = createTestMeterProvider();
const keyId = "https://server.example/users/alice#ed25519-key";
// Prime the cache with a wrong-algorithm key (the rsaPublicKey2 is RSA,
// not Ed25519) so that verifyProofInternal falls through to the
// fresh-fetch retry path.
const cache: Record<string, CryptographicKey | Multikey | null> = {
[keyId]: rsaPublicKey2,
};
const key = await verifyProof(jsonLd, proof, {
documentLoader: mockDocumentLoader,
contextLoader: mockDocumentLoader,
meterProvider,
keyCache: {
get(id) {
return Promise.resolve(cache[id.href]);
},
set(id, k) {
cache[id.href] = k;
return Promise.resolve();
},
} satisfies KeyCache,
});
assert(key != null);
assertEquals(
recorder.getMeasurements(
"activitypub.signature.verification.duration",
).length,
1,
);
const keyFetches = recorder.getMeasurements(
"activitypub.signature.key_fetch.duration",
);
assertEquals(keyFetches.length, 2);
assertEquals(
keyFetches[0].attributes["activitypub.signature.key_fetch.result"],
"hit",
);
assertEquals(
keyFetches[1].attributes["activitypub.signature.key_fetch.result"],
"fetched",
);
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/fedify/src/sig/proof.test.ts` around lines 663 - 693, Add assertions
that the key-fetch sequence produced two measurements: call
recorder.getMeasurements("activitypub.signature.key_fetch.duration"), assert its
length is 2, and assert the first measurement's attributes indicate the cache
"hit" and the second indicates "fetched" (e.g.,
measurements[0].attributes.result === "hit" and
measurements[1].attributes.result === "fetched") so the verifyProof retry path
(verifyProof / proof.ts logic) is validated to emit hit then fetched.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces two new OpenTelemetry histograms, activitypub.signature.verification.duration and activitypub.signature.key_fetch.duration, to monitor ActivityPub signature verification and public key lookup performance. The implementation spans HTTP Signatures, Linked Data Signatures, and Object Integrity Proofs, including updates to documentation and the addition of comprehensive test suites. The reviewer suggested an improvement to the getLdSignatureObject function in packages/fedify/src/sig/ld.ts to simplify type assertions and enhance readability by utilizing the object type for dynamic properties.

Comment on lines +462 to +474
function getLdSignatureObject(
jsonLd: unknown,
): Record<string, unknown> | undefined {
if (
typeof jsonLd === "object" && jsonLd != null && "signature" in jsonLd &&
typeof (jsonLd as { signature?: unknown }).signature === "object" &&
(jsonLd as { signature?: unknown }).signature != null &&
!Array.isArray((jsonLd as { signature?: unknown }).signature)
) {
return (jsonLd as { signature: Record<string, unknown> }).signature;
}
return undefined;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The getLdSignatureObject function can be simplified to improve readability and avoid repetitive type assertions. Additionally, use the object type instead of Record for dynamic properties to align with repository practices and reduce unnecessary type assertions.

function getLdSignatureObject(
  jsonLd: unknown,
): object | undefined {
  if (
    typeof jsonLd === "object" && jsonLd != null && "signature" in jsonLd
  ) {
    const { signature } = jsonLd as { signature: unknown };
    if (
      typeof signature === "object" &&
      signature != null &&
      !Array.isArray(signature)
    ) {
      return signature;
    }
  }
  return undefined;
}
References
  1. Avoid using Record types for dynamic properties where the structure can vary; prefer 'object' or more flexible types to prevent type errors and unnecessary assertions.

@chatgpt-codex-connector
Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Keep it up!

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

component/federation Federation object related component/otel OpenTelemetry integration component/signatures OIP or HTTP/LD Signatures related type/enhancement Improvements to existing features

Development

Successfully merging this pull request may close these issues.

OpenTelemetry metrics for signature verification duration

1 participant