Skip to content

Add signed ActivityPub server federation flows - #40

Closed
sij411 wants to merge 57 commits into
fedify-dev:mainfrom
sij411:feat/http-sig
Closed

Add signed ActivityPub server federation flows#40
sij411 wants to merge 57 commits into
fedify-dev:mainfrom
sij411:feat/http-sig

Conversation

@sij411

@sij411 sij411 commented Jul 27, 2026

Copy link
Copy Markdown
Member

Summary

Build out Feder's server runtime into a functional single-user ActivityPub federation proof of concept. The example project is a microblog called Federog. I follow fedify's microblog tutorial as a guideline.
This adds signed inbox (draft-cavage-http-signature way) and outbound delivery, follower management, persisted Notes, recipient-aware delivery, and ActivityPub object-serving safeguards while keeping protocol decisions in feder-core and platform concerns in the runtime.

Closes #26

Changes

  • Add actor RSA key generation, persistence, and public-key publication.
  • Sign outgoing ActivityPub requests using draft-Cavage HTTP signatures.
  • Verify incoming signed inbox requests and signing-key ownership.
  • Resolve remote actors and independent key URLs safely.
  • Harden outbound requests against private-network access, oversized responses,
    timeouts, and mismatched actor IDs.
  • Receive Follow and embedded Undo(Follow) activities.
  • Send signed Accept activities.
  • Persist follower relationships and expose the followers collection.
  • Persist local Notes and serve public Notes through ActivityPub object routes.
  • Negotiate ActivityPub response media types.
  • Resolve Note delivery from to ∪ cc.
  • Deliver to direct actors and current followers.
  • Deduplicate recipients and prefer shared inboxes for follower delivery.
  • Clear obsolete shared inboxes when refreshed actors remove them.
  • Return 404 for non-public Notes instead of exposing them anonymously.

Architecture

feder-core emits storage and delivery actions without performing network or database operations. The Axum/Tokio runtime resolves those actions using SQLite, remote actor discovery, HTTP signatures(draft-cavage-http-signature), and outbound delivery.

AI assistance is disclosed through the 'Assisted-by` trailers on the relevant commits. Changes section's contents are summarised by Codex:gpt-5.6-sol.

Summary by CodeRabbit

  • New Features

    • Added ActivityPub inbox and followers endpoints, including Follow and Undo(Follow) handling.
    • Added signed HTTP request support with RSA keys, digest validation, and configurable inbox authentication.
    • Added persistent SQLite storage for actors, keys, followers, and notes.
    • Added note creation and delivery to followers or direct recipients.
    • Added actor discovery, public-key resolution, content negotiation, and outbound address protections.
    • Added a runnable single-user server example and expanded ActivityPub vocabulary support.
  • Documentation

    • Updated server documentation with configuration, storage, delivery, and example usage.

sij411 added 30 commits July 16, 2026 17:11
  Move the hardcoded single-user demo into examples/single-user-server
  and
  leave feder-runtime-server as reusable Axum/Tokio integration. Expose
  the
  router/config building blocks, remove default local demo config from
  the
  library, and test the reusable routes directly.

  Assisted-by: Codex:gpt-5.5
  Covers invalid inbox targets, unsupported content types, malformed JSON,
  unsupported activities, and oversized request bodies.

  Assisted-by: Codex:gpt-5.5
  Reject unsigned inbox requests before core handling when signed inbox
  auth is required, while keeping explicit insecure dev mode for tests
  and the single-user example.

  Assisted-by: Codex:gpt-5.5
…ding

  the runtime inbox auth policy.

  Assisted-by: Codex:gpt-5.5
Assisted-by: Codex:gpt-5
Assisted-by: Codex:gpt-5.5
  Use the RuntimeStore persist_actions method from the inbox handler and
  initialize the runtime AppState with an in-memory SQLite store for now.

  Verified with:
  cargo +1.95.0 check -p feder-runtime-server

  Assisted-by: Codex:gpt-5.5
  Enable rusqlite's bundled SQLite feature so feder-runtime-server tests do not
  depend on a system libsqlite3 installation.

  Verified with:
  cargo +1.95.0 test -p feder-runtime-server

  Assisted-by: Codex:gpt-5.5
  Pin the repository to Rust 1.95.0 so rusqlite 0.40.1 builds consistently.
  Add focused SQLite store tests for schema initialization, follower
  persistence,
  and duplicate follower idempotency.

  Verified with:
  cargo test -p feder-runtime-server

  Assisted-by: Codex:gpt-5.5
  Load follower rows from the SQLite store into StoredState and parse stored
  actor IDs back into ActivityPub IRIs. Add coverage for follower restoration.

  Verified with:
  cargo test -p feder-runtime-server

  Assisted-by: Codex:gpt-5.5
  Extend the followers table with inbox fields and persist an embedded follower
  actor's inbox URL with the follower relationship. This keeps recipient
  delivery
  data attached to follower records, matching Fedify's follower-recipient model.

  Verified with:
  cargo test -p feder-runtime-server

  Assisted-by: Codex:gpt-5.5
  Represent stored followers as recipient records with optional inbox fields and  restore those fields from SQLite. This preserves follower delivery data loaded  from embedded actor records.

  Verified with:
  cargo test -p feder-runtime-server

  Assisted-by: Codex:gpt-5.5
  Replace the restore-style load_state API with load_followers so runtime
  storage
  can expose durable follower data without implying full core state restoration.
  Assisted-by: Codex:gpt-5.5
  Replace the restore-style follower read with actor-scoped list_followers, and
  add list_follower_recipients for delivery-ready follower inbox lookup.

  Verified with:
  cargo test -p feder-runtime-server

  Assisted-by: Codex:gpt-5.5
  Add StorageConfig to choose in-memory or file-backed SQLite storage, and make
  router construction return storage initialization errors. Cover file-backed
  storage with a reopen test for persisted follower state.

  Assisted-by: Codex:gpt-5.5
  Add minimal actor endpoints vocabulary support for endpoints.sharedInbox and
  store embedded follower shared inbox URLs in SQLite follower records.

  Assisted-by: Codex:gpt-5.5
  Persist delivery target actions into follower inbox records, add an index for
  scoped follower lookups, and avoid holding the core lock during storage
  writes.

  Assisted-by: Codex:gpt-5.5
Add the security JSON-LD context and model embedded CryptographicKey values
on ActivityPub actors. Cover the publicKey representation with serialization
tests.

Assisted-by: Codex:gpt-5.6-sol
…r Core.

Accept runtime-provided entropy, validate persisted pairs, and zeroize
private PEM material. Use fixed fixtures for behavioral tests and exercise
all workspace features and targets in CI.

Assisted-by: Codex:gpt-5.6-sol
Enable HTTP signature keys in the server runtime and store validated per-actor
key pairs in the keys table. Reject accidental replacement and test schema
creation, round trips, malformed keys, missing actors, and persistence across
reopen.

Assisted-by: Codex:gpt-5.6-sol
Move HTTP endpoint and startup tests under the tests directory, with shared
fixtures kept private to the integration test crate.

Assisted-by: Codex:gpt-5.6-sol
Generate and persist a 4096-bit RSA key pair with operating system entropy
when an actor has no stored key, and reuse the persisted pair on restart.

Assisted-by: Codex:gpt-5.6-sol
Attach the persisted RSA public key to the local actor as an embedded
CryptographicKey using the #main-key identifier and security context.

Assisted-by: Codex:gpt-5.6-sol
Deliver SendActivity actions synchronously to recipient inboxes as ActivityPub
JSON, report remote failures, and continue attempting later recipients after
individual delivery failures.

Assisted-by: Codex:gpt-5.6-sol
Create draft-Cavage RSA signatures in the portable core and
apply them to outgoing ActivityPub requests using the persisted actor key and
matching #main-key identifier.

Assisted-by: Codex:gpt-5.6-sol
sij411 added 26 commits July 28, 2026 03:25
Block private and special-use inbox destinations using validated DNS
resolution, with an explicit option for local development. Configure
connection and total request timeouts to prevent stalled deliveries.

Assisted-by: Codex:gpt-5.6-sol
…vered

inbox, and deliver the signed Accept activity. Reuse outbound network
protections and enforce response type, size, and actor ID validation.

Add an end-to-end test covering Mastodon-style actor resolution and Accept
delivery.

Assisted-by: Codex:gpt-5.6-sol
Validate Cavage RSA-SHA256 signatures, signed headers, request dates, body
digests, actor ownership, and public-key identity before processing inbox
activities.

Add tests for valid, tampered, unsigned, and mismatched-actor requests.

Assisted-by: Codex:gpt-5.6-sol
Fetch signature key IDs directly and support standalone key documents as well
as keys embedded in actor documents. Verify that the activity actor owns and
advertises the resolved key before processing the request.

Add coverage for signed Follows using an independent key URL.

Assisted-by: Codex:gpt-5.6-sol
Default omitted public key types to CryptographicKey for compatibility with
Mastodon actor documents. Cover the representation in vocabulary and signed
Follow integration tests.

Assisted-by: Codex:gpt-5.6-sol
Add Undo vocabulary and core handling for follower removal. Validate that the
Undo actor owns the embedded Follow, remove matching in-memory and SQLite
follower state, and cover signed unfollow and forged Undo requests.

Assisted-by: Codex:gpt-5.6-sol
Add the OrderedCollection vocabulary type and followers property for actors.
Advertise the local actor's followers collection URI and preserve the property
when resolving remote actors.

Assisted-by: Codex:gpt-5.6-sol
Add a storage-backed followers handler and expose it through the runtime
router. Return an ActivityPub OrderedCollection with current follower IDs and
counts, and cover collection updates, response headers, and unknown users.

Assisted-by: Codex:gpt-5.6-sol
Require an explicit ActivityPub-compatible Accept header for actor and
followers collection requests. Return 406 for HTML, wildcard-only, missing,
or unsupported Accept headers, and mark successful responses with Vary:
Accept.

Add coverage for supported media types, quality preferences, HTML requests,
and requests without an ActivityPub Accept header.

Assisted-by: Codex:gpt-5.6-sol
Add Vary: Accept to actor and followers responses that reject requests
without an ActivityPub-compatible Accept header, preventing caches from
reusing those responses across representations.

Assisted-by: Codex:gpt-5.6-sol
Ignore unsupported Accept media types when choosing between HTML and
ActivityPub. Compare the best quality in each supported representation group
and use header order to resolve equal preferences.

Assisted-by: Codex:gpt-5.6-sol
Add to, cc, url, and mediaType fields to the Note vocabulary model. Preserve
ActivityStreams scalar-or-array recipient serialization and omit empty or
absent values.

Assisted-by: Codex:gpt-5.6-sol
Store Note objects emitted through StoreObject actions in SQLite using their
IRI, object type, and serialized payload. Add typed object loading, idempotent
replacement, and persistence across database reopen.

Assisted-by: Codex:gpt-5.6-sol
Add the local post object route with ActivityPub content negotiation,
SQLite-backed lookup, and coverage for persistence and error responses.

Assisted-by: Codex:gpt-5.6-sol
Add a runtime operation for local Note creation that applies storage
actions before synchronously delivering Create activities. Reuse the same
action pipeline for inbox processing and cover successful and failed delivery.

Assisted-by: Codex:gpt-5.6-sol
Remove the in-memory delivery target cache and resolve current follower
inboxes from SQLite when delivering Create activities. Keep delivery
synchronous, deduplicate shared inboxes, and refresh inbox data through
repeated Follow persistence.

Assisted-by: Codex:gpt-5.6-sol
Populate Note addressing and metadata, mirror to and cc onto Create,
and use one SendActivity action with typed inbox or follower recipients.
Resolve follower recipients into concrete inbox deliveries at runtime.

Assisted-by: Codex:gpt-5.6-sol
Assisted-by: Codex:gpt-5.6-sol
Assisted-by: Codex:gpt-5.6-sol
Assisted-by: Codex:gpt-5.6-sol
Assisted-by: Codex:gpt-5.6-sol
@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds authenticated ActivityPub federation support across vocabulary, core state handling, runtime routing, actor resolution, SQLite persistence, signed delivery, inbox verification, object/follower endpoints, integration tests, and a single-user example server.

Changes

ActivityPub vocabulary and core delivery

Layer / File(s) Summary
Vocabulary and core contracts
crates/feder-vocab/src/lib.rs, crates/feder-core/src/lib.rs, crates/feder-vocab/tests/*
Adds cryptographic keys, security context handling, Undo, OrderedCollection, recipient fields, undo-follow inputs, recipient-based delivery actions, and public-collection filtering.
HTTP signatures and remote actors
crates/feder-core/src/http_signatures.rs, crates/feder-runtime-server/src/actor.rs, crates/feder-runtime-server/src/url.rs
Adds RSA key generation/loading, Draft-Cavage signing and verification, remote actor/key resolution, response limits, content checks, and outbound address validation.
Storage and outbound delivery
crates/feder-runtime-server/src/storage/*, crates/feder-runtime-server/src/operation.rs, crates/feder-runtime-server/src/send.rs
Adds SQLite persistence for followers, objects, and actor keys; expands recipient actions; and sends signed Accept, Follow, and Create requests.
Runtime configuration and endpoints
crates/feder-runtime-server/src/app.rs, config.rs, inbox.rs, followers.rs, object.rs, negotiation.rs, lib.rs
Adds storage and authentication policies, router wiring, ActivityPub negotiation, actor/follower/object responses, and validated Follow/Undo inbox processing.
Validation and example server
crates/feder-runtime-server/tests/*, examples/single-user-server/*, mise.toml, rust-toolchain.toml
Adds integration coverage for federation flows and persistence, moves runtime tests into cases, adds a runnable single-user server, and broadens workspace checks.
Documentation and dependency wiring
Cargo.toml, crates/feder-core/Cargo.toml, crates/feder-runtime-server/Cargo.toml, crates/feder-runtime-server/README.md
Registers the example crate, configures optional HTTP-signature dependencies, updates runtime dependencies, and documents the expanded server behavior.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

  • fedify-dev/feder#13: Extends the same ActivityPub vocabulary model with security context, cryptographic keys, Undo, and ordered collections.
  • fedify-dev/feder#16: Provides the earlier core input/action boundary that this change reshapes for undo and recipient-based delivery.
  • fedify-dev/feder#28: Introduces the runtime-server foundation that this change expands with storage, keys, routing, and federation behavior.

Suggested labels: enhancement

Suggested reviewers: dahlia

Sequence Diagram(s)

sequenceDiagram
  participant RemoteActor
  participant Inbox
  participant ActorResolver
  participant FederCore
  participant RuntimeStore
  participant ActivitySender
  participant RemoteInbox

  RemoteActor->>Inbox: POST Follow or Undo(Follow)
  Inbox->>ActorResolver: resolve actor and signing key
  ActorResolver-->>Inbox: verified actor
  Inbox->>FederCore: handle received input
  FederCore->>RuntimeStore: persist follower action
  FederCore-->>Inbox: Accept delivery action
  Inbox->>ActivitySender: send resolved activity
  ActivitySender->>RemoteInbox: signed POST Accept
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 58.95% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: signed ActivityPub federation flows for the server.
Linked Issues check ✅ Passed The PR covers #26 with actor/key resolution, signature verification, signed outbound delivery, safe fetch checks, and tests for invalid signatures and unsafe targets.
Out of Scope Changes check ✅ Passed The added runtime, demo, and documentation files support the federation flow and are not clearly unrelated to #26.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch feat/http-sig

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (8)
crates/feder-runtime-server/src/followers.rs (1)

39-56: 🚀 Performance & Scalability | 🔵 Trivial

Unpaginated followers collection.

Every request loads and serializes all followers. Fine for the single-user PoC, but ActivityPub consumers expect an OrderedCollection with first/next pages once the follower count grows; worth tracking before this leaves proof-of-concept status.

🤖 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 `@crates/feder-runtime-server/src/followers.rs` around lines 39 - 56, Update
the followers collection flow around OrderedCollection::new to support paginated
ActivityPub responses instead of always loading and serializing every follower.
Add first/next page metadata and limit store retrieval to the requested page,
while preserving the existing collection ID and total follower count behavior.
crates/feder-runtime-server/src/inbox.rs (1)

283-303: 🎯 Functional Correctness | 🔵 Trivial

Accept bare signature parameter values while parsing the header.

parse_signature_header requires every parameter value to start with a quote, so signatures like algorithm=rsa-sha256,headers=...,signature=... are still rejected even though the algorithm itself is validated later. Since the current code already checks algorithm and signed-headers case-insensitively and lowercases algorithm, accepting non-quoted values for the header parser would not weaken signature semantics.
[low_effort_and_reward]

🤖 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 `@crates/feder-runtime-server/src/inbox.rs` around lines 283 - 303, Update
parse_signature_header to accept unquoted parameter values in addition to quoted
values, while preserving existing quoted parsing and validation behavior. Reuse
the existing algorithm and signed-headers case-insensitive checks and algorithm
lowercasing so signature semantics remain unchanged; ensure bare values
terminate at the parameter delimiter.
crates/feder-runtime-server/tests/cases/followers.rs (1)

28-30: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Duplicated iri() test helper across three cases files. All three files define the identical private helper fn iri(value: &str) -> Iri { value.parse().expect("valid test IRI") }; each already imports from crate::common, so this is a good candidate for a single shared helper.

  • crates/feder-runtime-server/tests/cases/followers.rs#L28-L30: remove the local iri() and import a shared version from crate::common.
  • crates/feder-runtime-server/tests/cases/object.rs#L29-L31: remove the local iri() and import the same shared helper.
  • crates/feder-runtime-server/tests/cases/operation.rs#L23-L25: remove the local iri() and import the same shared helper.
♻️ Suggested consolidation (add to tests/common/mod.rs)
pub fn iri(value: &str) -> feder_vocab::Iri {
    value.parse().expect("valid test IRI")
}
🤖 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 `@crates/feder-runtime-server/tests/cases/followers.rs` around lines 28 - 30,
Consolidate the duplicated iri test helper by adding one shared public iri
function to tests/common/mod.rs, then remove the local iri definitions and
import the shared helper in crates/feder-runtime-server/tests/cases/followers.rs
(lines 28-30), crates/feder-runtime-server/tests/cases/object.rs (lines 29-31),
and crates/feder-runtime-server/tests/cases/operation.rs (lines 23-25).
crates/feder-runtime-server/tests/cases/send.rs (1)

165-177: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Assert the terminal delivery error variant.

result.is_err() permits an unrelated failure to satisfy this test. Assert UnsuccessfulStatus for failed_inbox and HTTP 500 to preserve the status-mapping contract in crates/feder-runtime-server/src/send.rs.

Proposed test assertion
-    assert!(result.is_err());
+    assert!(matches!(
+        result,
+        Err(SendError::UnsuccessfulStatus { inbox, status })
+            if inbox == failed_inbox && status == StatusCode::INTERNAL_SERVER_ERROR
+    ));
🤖 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 `@crates/feder-runtime-server/tests/cases/send.rs` around lines 165 - 177,
Update attempts_later_sends_after_failure to assert that the returned error is
the UnsuccessfulStatus variant for failed_inbox and contains HTTP status 500,
rather than only checking result.is_err(). Preserve the existing actions and
successful follow-up send coverage.
crates/feder-runtime-server/src/storage/sqlite.rs (1)

49-75: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Consider WAL and a busy timeout for the file-backed store.

Connection::open defaults to rollback journal with no busy timeout, so any second process or future second connection gets immediate SQLITE_BUSY. PRAGMA journal_mode=WAL; PRAGMA busy_timeout=5000; in init is a cheap hedge.

🤖 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 `@crates/feder-runtime-server/src/storage/sqlite.rs` around lines 49 - 75,
Update the SQLite initialization flow in `init` to configure the file-backed
connection with WAL journaling and a 5-second busy timeout before creating the
tables. Apply both pragmas through the existing `self.conn` setup while
preserving the current schema creation and error propagation.
crates/feder-core/src/http_signatures.rs (2)

123-134: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Parsing the PKCS#8 private key on every signature is avoidable work.

Each outbound delivery re-parses a 4096-bit private key. Consider caching a lazily-built SigningKey<Sha256> inside ActorKeyPair (e.g. OnceLock) or exposing a Signer handle so signing hot paths only pay the RSA operation.

🤖 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 `@crates/feder-core/src/http_signatures.rs` around lines 123 - 134, Update
ActorKeyPair and sign_draft so the PKCS#8 private key is parsed once into a
lazily initialized cached SigningKey<Sha256> (or equivalent signer handle), then
reuse that cached signer for subsequent signatures while preserving
InvalidPrivateKey error handling during initialization.

173-189: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Header values with multiple occurrences are not comma-joined.

Draft-Cavage requires repeated headers to be combined as a single comma-separated line. The base builder emits one line per tuple, so a caller passing a duplicated header name will produce a signature peers cannot reproduce. Worth documenting the caller contract at minimum.

🤖 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 `@crates/feder-core/src/http_signatures.rs` around lines 173 - 189, Update
draft_cavage_signature_base to group entries with the same header name and
combine their trimmed values into one comma-separated line, while preserving
lowercase names and existing ordering semantics. Ensure repeated headers no
longer produce multiple signature-base lines, and document the expected
duplicate-header handling contract if the caller must provide pre-grouped
values.
crates/feder-runtime-server/src/operation.rs (1)

31-48: 🩺 Stability & Availability | 🔵 Trivial | 🏗️ Heavy lift

Outbound delivery has no durable status or retry, so failures are both fatal to the request and partly invisible. Both sites stem from one root cause: delivery is performed inline with no record of per-recipient outcome.

  • crates/feder-runtime-server/src/operation.rs#L31-L48: stop propagating send_actions errors out of handle_input; record delivery outcome instead so a single unreachable inbox does not fail an already-persisted operation.
  • crates/feder-runtime-server/src/send.rs#L57-L69: return or log every per-recipient error rather than only the first, so partial fan-out failure is observable.
🤖 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 `@crates/feder-runtime-server/src/operation.rs` around lines 31 - 48, Update
crates/feder-runtime-server/src/operation.rs lines 31-48 in handle_input so
activity_sender.send_actions failures are recorded or handled without
propagating them as a request error after actions are persisted; update
crates/feder-runtime-server/src/send.rs lines 57-69 in resolve outbound delivery
fan-out to return or log every per-recipient error instead of stopping at the
first failure, preserving visibility of partial delivery failures.
🤖 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 `@crates/feder-core/src/lib.rs`:
- Around line 255-275: Update the delivery flow that expands Recipients after
note_recipients so it deduplicates recipients by resolved inbox URL before
enqueueing sends. Preserve note_recipients’ existing address-level
deduplication, but ensure an actor reached directly and through
Recipients::Followers produces only one delivery.

In `@crates/feder-runtime-server/src/inbox.rs`:
- Around line 110-118: In the request verification flow around
verify_request_date and verify_request_digest, validate the request’s signed
host header against the configured handle_host or bind host before accepting the
signature. Reject mismatches with the existing unauthorized response, while
preserving verification for requests addressed to this server.

In `@crates/feder-runtime-server/src/url.rs`:
- Around line 36-66: Add the deprecated IPv6 site-local CIDR "fec0::/10" to
NON_PUBLIC_NETWORK_CIDRS, preserving the existing blocklist structure and
entries.

In `@crates/feder-vocab/src/lib.rs`:
- Around line 228-243: Update CryptographicKeyType deserialization so present
unknown values, including strings such as "Key" and arrays, resolve to the
default CryptographicKey variant instead of failing; preserve the existing
default for missing type and the current enum behavior for recognized values.
- Around line 180-209: Update the Context deserialization used by ActorDocument
so JSON-LD context arrays containing object or other non-IRI entries are
accepted and preserved or safely ignored while IRI entries continue mapping to
Context::Iri or Context::Iris. Extend Context with an ignored term/object
variant or implement a custom deserializer, ensuring Mastodon-style contexts
deserialize without rejecting the actor.

---

Nitpick comments:
In `@crates/feder-core/src/http_signatures.rs`:
- Around line 123-134: Update ActorKeyPair and sign_draft so the PKCS#8 private
key is parsed once into a lazily initialized cached SigningKey<Sha256> (or
equivalent signer handle), then reuse that cached signer for subsequent
signatures while preserving InvalidPrivateKey error handling during
initialization.
- Around line 173-189: Update draft_cavage_signature_base to group entries with
the same header name and combine their trimmed values into one comma-separated
line, while preserving lowercase names and existing ordering semantics. Ensure
repeated headers no longer produce multiple signature-base lines, and document
the expected duplicate-header handling contract if the caller must provide
pre-grouped values.

In `@crates/feder-runtime-server/src/followers.rs`:
- Around line 39-56: Update the followers collection flow around
OrderedCollection::new to support paginated ActivityPub responses instead of
always loading and serializing every follower. Add first/next page metadata and
limit store retrieval to the requested page, while preserving the existing
collection ID and total follower count behavior.

In `@crates/feder-runtime-server/src/inbox.rs`:
- Around line 283-303: Update parse_signature_header to accept unquoted
parameter values in addition to quoted values, while preserving existing quoted
parsing and validation behavior. Reuse the existing algorithm and signed-headers
case-insensitive checks and algorithm lowercasing so signature semantics remain
unchanged; ensure bare values terminate at the parameter delimiter.

In `@crates/feder-runtime-server/src/operation.rs`:
- Around line 31-48: Update crates/feder-runtime-server/src/operation.rs lines
31-48 in handle_input so activity_sender.send_actions failures are recorded or
handled without propagating them as a request error after actions are persisted;
update crates/feder-runtime-server/src/send.rs lines 57-69 in resolve outbound
delivery fan-out to return or log every per-recipient error instead of stopping
at the first failure, preserving visibility of partial delivery failures.

In `@crates/feder-runtime-server/src/storage/sqlite.rs`:
- Around line 49-75: Update the SQLite initialization flow in `init` to
configure the file-backed connection with WAL journaling and a 5-second busy
timeout before creating the tables. Apply both pragmas through the existing
`self.conn` setup while preserving the current schema creation and error
propagation.

In `@crates/feder-runtime-server/tests/cases/followers.rs`:
- Around line 28-30: Consolidate the duplicated iri test helper by adding one
shared public iri function to tests/common/mod.rs, then remove the local iri
definitions and import the shared helper in
crates/feder-runtime-server/tests/cases/followers.rs (lines 28-30),
crates/feder-runtime-server/tests/cases/object.rs (lines 29-31), and
crates/feder-runtime-server/tests/cases/operation.rs (lines 23-25).

In `@crates/feder-runtime-server/tests/cases/send.rs`:
- Around line 165-177: Update attempts_later_sends_after_failure to assert that
the returned error is the UnsuccessfulStatus variant for failed_inbox and
contains HTTP status 500, rather than only checking result.is_err(). Preserve
the existing actions and successful follow-up send coverage.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e4967f2d-59cc-46d9-8778-e0dd4ca0fa2c

📥 Commits

Reviewing files that changed from the base of the PR and between 8843ea6 and b029a4c.

⛔ Files ignored due to path filters (6)
  • Cargo.lock is excluded by !**/*.lock
  • crates/feder-core/tests/fixtures/rsa-other-public-key.pem is excluded by !**/*.pem
  • crates/feder-core/tests/fixtures/rsa-private-key.pem is excluded by !**/*.pem
  • crates/feder-core/tests/fixtures/rsa-public-key.pem is excluded by !**/*.pem
  • crates/feder-runtime-server/tests/fixtures/rsa-private-key.pem is excluded by !**/*.pem
  • crates/feder-runtime-server/tests/fixtures/rsa-public-key.pem is excluded by !**/*.pem
📒 Files selected for processing (39)
  • Cargo.toml
  • crates/feder-core/Cargo.toml
  • crates/feder-core/src/http_signatures.rs
  • crates/feder-core/src/lib.rs
  • crates/feder-runtime-server/Cargo.toml
  • crates/feder-runtime-server/README.md
  • crates/feder-runtime-server/src/actor.rs
  • crates/feder-runtime-server/src/app.rs
  • crates/feder-runtime-server/src/config.rs
  • crates/feder-runtime-server/src/error.rs
  • crates/feder-runtime-server/src/followers.rs
  • crates/feder-runtime-server/src/inbox.rs
  • crates/feder-runtime-server/src/lib.rs
  • crates/feder-runtime-server/src/negotiation.rs
  • crates/feder-runtime-server/src/note.rs
  • crates/feder-runtime-server/src/object.rs
  • crates/feder-runtime-server/src/operation.rs
  • crates/feder-runtime-server/src/send.rs
  • crates/feder-runtime-server/src/storage/mod.rs
  • crates/feder-runtime-server/src/storage/sqlite.rs
  • crates/feder-runtime-server/src/url.rs
  • crates/feder-runtime-server/src/webfinger.rs
  • crates/feder-runtime-server/tests/cases/actor.rs
  • crates/feder-runtime-server/tests/cases/app.rs
  • crates/feder-runtime-server/tests/cases/followers.rs
  • crates/feder-runtime-server/tests/cases/inbox.rs
  • crates/feder-runtime-server/tests/cases/object.rs
  • crates/feder-runtime-server/tests/cases/operation.rs
  • crates/feder-runtime-server/tests/cases/send.rs
  • crates/feder-runtime-server/tests/cases/webfinger.rs
  • crates/feder-runtime-server/tests/common/mod.rs
  • crates/feder-runtime-server/tests/runtime.rs
  • crates/feder-vocab/src/lib.rs
  • crates/feder-vocab/tests/activitypub_serialization.rs
  • examples/single-user-server/Cargo.toml
  • examples/single-user-server/README.md
  • examples/single-user-server/src/main.rs
  • mise.toml
  • rust-toolchain.toml
💤 Files with no reviewable changes (2)
  • crates/feder-runtime-server/src/note.rs
  • crates/feder-runtime-server/src/webfinger.rs

Comment on lines +255 to +275
fn note_recipients(local_actor: &vocab::Actor, note: &vocab::Note) -> Vec<Recipients> {
let mut recipients = Vec::new();

for address in note.to.iter().chain(note.cc.iter()) {
let recipient = if address.as_str() == PUBLIC_COLLECTION {
// Public describes visibility. It cannot receive an activity.
continue;
} else if local_actor.followers.as_ref() == Some(address) {
Recipients::Followers(local_actor.id.clone())
} else if address == &local_actor.id {
continue;
} else {
Recipients::Actor(address.clone())
};

if !recipients.contains(&recipient) {
recipients.push(recipient);
}
}
recipients
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect runtime recipient expansion for inbox-level dedup
fd -t f 'operation.rs' crates/feder-runtime-server/src | xargs -r ast-grep outline --items all
rg -nP -C5 'Recipients::(Followers|Actor|Inbox)' crates/feder-runtime-server/src

Repository: fedify-dev/feder

Length of output: 4574


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files =="
fd -t f 'operation.rs|lib.rs|send.rs' crates

echo "== operation.rs relevant section =="
sed -n '1,120p' crates/feder-runtime-server/src/operation.rs | cat -n

echo "== resolver definitions/usages =="
fd -t f . crates | rg 'actor|resolver|resolver' | sed -n '1,80p'
rg -n "struct .*Resolver|trait .*Resolver|resolve\\(" crates/feder-runtime-server/src crates/feder-core/src | sed -n '1,120p'

echo "== note_recipients context =="
sed -n '220,290p' crates/feder-core/src/lib.rs | cat -n

echo "== SendActivity recipients construction sites =="
rg -n "SendActivity\\{|recipients:|note_recipients|to_recipients" crates -C 4

Repository: fedify-dev/feder

Length of output: 13995


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== runtime send implementation =="
sed -n '1,180p' crates/feder-runtime-server/src/send.rs | cat -n

echo "== Resolve recipients tests =="
sed -n '1,260p' crates/feder-runtime-server/tests/cases/operation.rs | cat -n

echo "== actor resolver relevant section =="
sed -n '1,140p' crates/feder-runtime-server/src/actor.rs | cat -n

Repository: fedify-dev/feder

Length of output: 21569


Deduplicate deliveries by resolved inbox before sending.

Recipients::Followers dedupes only within the follower list, while Recipients::Actor resolves each actor directly and adds it to the delivery queue. If bob is directly addressed and also listed as a follower, he gets one delivery from the actor path and another from the followers path unless runtime-level inbox dedup is added after _followers_ expansion.

🤖 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 `@crates/feder-core/src/lib.rs` around lines 255 - 275, Update the delivery
flow that expands Recipients after note_recipients so it deduplicates recipients
by resolved inbox URL before enqueueing sends. Preserve note_recipients’
existing address-level deduplication, but ensure an actor reached directly and
through Recipients::Followers produces only one delivery.

Comment on lines +110 to +118
if !["host", "date", "digest"]
.iter()
.all(|required| seen_headers.contains(required))
{
return Err(StatusCode::UNAUTHORIZED);
}

verify_request_date(&req.headers)?;
verify_request_digest(&req.headers, &req.body)?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Signed host value is never compared against this server's host.

host is required to be signed, but its value is taken from the request and never checked against the configured handle_host/bind host. A signature captured for another destination remains verifiable when replayed here, since both the signing string and the verification use the attacker-supplied host. Today the blast radius is limited by the follow.object == local_actor.id check, but the binding should be explicit.

🔒 Proposed check
     if !["host", "date", "digest"]
         .iter()
         .all(|required| seen_headers.contains(required))
     {
         return Err(StatusCode::UNAUTHORIZED);
     }
+    let host = req
+        .headers
+        .get("host")
+        .and_then(|value| value.to_str().ok())
+        .ok_or(StatusCode::UNAUTHORIZED)?;
+    if !host.eq_ignore_ascii_case(&app_state.handle_host) {
+        return Err(StatusCode::UNAUTHORIZED);
+    }
📝 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
if !["host", "date", "digest"]
.iter()
.all(|required| seen_headers.contains(required))
{
return Err(StatusCode::UNAUTHORIZED);
}
verify_request_date(&req.headers)?;
verify_request_digest(&req.headers, &req.body)?;
if !["host", "date", "digest"]
.iter()
.all(|required| seen_headers.contains(required))
{
return Err(StatusCode::UNAUTHORIZED);
}
let host = req
.headers
.get("host")
.and_then(|value| value.to_str().ok())
.ok_or(StatusCode::UNAUTHORIZED)?;
if !host.eq_ignore_ascii_case(&app_state.handle_host) {
return Err(StatusCode::UNAUTHORIZED);
}
verify_request_date(&req.headers)?;
verify_request_digest(&req.headers, &req.body)?;
🤖 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 `@crates/feder-runtime-server/src/inbox.rs` around lines 110 - 118, In the
request verification flow around verify_request_date and verify_request_digest,
validate the request’s signed host header against the configured handle_host or
bind host before accepting the signature. Reject mismatches with the existing
unauthorized response, while preserving verification for requests addressed to
this server.

Comment on lines +36 to +66
const NON_PUBLIC_NETWORK_CIDRS: &[&str] = &[
"0.0.0.0/8",
"10.0.0.0/8",
"100.64.0.0/10",
"127.0.0.0/8",
"169.254.0.0/16",
"172.16.0.0/12",
"192.0.0.0/24",
"192.0.2.0/24",
"192.88.99.0/24",
"192.168.0.0/16",
"198.18.0.0/15",
"198.51.100.0/24",
"203.0.113.0/24",
"224.0.0.0/4",
"240.0.0.0/4",
"::/128",
"::1/128",
"64:ff9b::/96",
"64:ff9b:1::/48",
"100::/64",
"100:0:0:1::/64",
"2001::/23",
"2001:db8::/32",
"2002::/16",
"3fff::/20",
"5f00::/16",
"fc00::/7",
"fe80::/10",
"ff00::/8",
];

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Consider adding deprecated IPv6 site-local fec0::/10.

fc00::/7 covers ULA but not the deprecated site-local range, which some internal networks still use. Cheap to add to the blocklist.

🛡️ Proposed addition
     "fc00::/7",
+    "fec0::/10",
     "fe80::/10",
📝 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
const NON_PUBLIC_NETWORK_CIDRS: &[&str] = &[
"0.0.0.0/8",
"10.0.0.0/8",
"100.64.0.0/10",
"127.0.0.0/8",
"169.254.0.0/16",
"172.16.0.0/12",
"192.0.0.0/24",
"192.0.2.0/24",
"192.88.99.0/24",
"192.168.0.0/16",
"198.18.0.0/15",
"198.51.100.0/24",
"203.0.113.0/24",
"224.0.0.0/4",
"240.0.0.0/4",
"::/128",
"::1/128",
"64:ff9b::/96",
"64:ff9b:1::/48",
"100::/64",
"100:0:0:1::/64",
"2001::/23",
"2001:db8::/32",
"2002::/16",
"3fff::/20",
"5f00::/16",
"fc00::/7",
"fe80::/10",
"ff00::/8",
];
const NON_PUBLIC_NETWORK_CIDRS: &[&str] = &[
"0.0.0.0/8",
"10.0.0.0/8",
"100.64.0.0/10",
"127.0.0.0/8",
"169.254.0.0/16",
"172.16.0.0/12",
"192.0.0.0/24",
"192.0.2.0/24",
"192.88.99.0/24",
"192.168.0.0/16",
"198.18.0.0/15",
"198.51.100.0/24",
"203.0.113.0/24",
"224.0.0.0/4",
"240.0.0.0/4",
"::/128",
"::1/128",
"64:ff9b::/96",
"64:ff9b:1::/48",
"100::/64",
"100:0:0:1::/64",
"2001::/23",
"2001:db8::/32",
"2002::/16",
"3fff::/20",
"5f00::/16",
"fc00::/7",
"fec0::/10",
"fe80::/10",
"ff00::/8",
];
🤖 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 `@crates/feder-runtime-server/src/url.rs` around lines 36 - 66, Add the
deprecated IPv6 site-local CIDR "fec0::/10" to NON_PUBLIC_NETWORK_CIDRS,
preserving the existing blocklist structure and entries.

Comment on lines +180 to +209
/// A JSON-LD context represented by one or more IRIs.
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(untagged)]
pub enum Context {
Iri(Iri),
Iris(Vec<Iri>),
}

impl Context {
#[must_use]
pub fn one(context: Iri) -> Self {
Self::Iri(context)
}

#[must_use]
pub fn many(contexts: impl Into<Vec<Iri>>) -> Self {
Self::Iris(contexts.into())
}

fn include(&mut self, context: Iri) {
match self {
Self::Iri(existing) if existing == &context => {}
Self::Iri(existing) => {
*self = Self::Iris(Vec::from([existing.clone(), context]));
}
Self::Iris(existing) if !existing.contains(&context) => existing.push(context),
Self::Iris(_) => {}
}
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Where are remote actors deserialized into vocab::Actor?
rg -nP -C4 'from_slice::<Actor>|from_value::<Actor>|: Actor =' crates/feder-runtime-server/src
# Confirm the fixture context shape used by inbox tests
rg -nP -C3 '`@context`' crates/feder-runtime-server/tests/cases/inbox.rs

Repository: fedify-dev/feder

Length of output: 1813


🏁 Script executed:

#!/bin/bash
set -u

printf '--- files mentioning deserialize Actor/context ---\n'
rg -n 'Actor|Context|Deserialize' crates/feder-vocab crates/feder-runtime-server/src crates/feder-runtime-server/tests/cases/inbox.rs

printf '\n--- actor resolver-related mentions ---\n'
rg -n 'actor|resolve|Deserialize|from_slice|from_value' crates/feder-runtime-server/src crates/feder-vocab/src -g '*.rs'

printf '\n--- vocab Context definition and Actor fields ---\n'
sed -n '30,60p;160,215p' crates/feder-vocab/src/lib.rs

printf '\n--- inbox fixture around 130 ---\n'
sed -n '110,145p' crates/feder-runtime-server/tests/cases/inbox.rs

Repository: fedify-dev/feder

Length of output: 46123


🏁 Script executed:

#!/bin/bash
set -u

printf '%s\n' '--- crates/feder-runtime-server/src/actor.rs relevant slices ---'
sed -n '75,102p' crates/feder-runtime-server/src/actor.rs
sed -n '191,218p' crates/feder-runtime-server/src/actor.rs

printf '%s\n' '--- crates/feder-runtime-server/src/inbox.rs accept/follow handling ---'
sed -n '138,172p' crates/feder-runtime-server/src/inbox.rs
sed -n '330,370p' crates/f feder-runtime-server/src/inbox.rs

printf '%s\n' '--- all follow/accept tests in inbox.rs around fixture usage ---'
rg -n -C6 'accepts/follow|inbox/follow|follow-1|follow-2|follows|ActorDocument|ActorType|ActivityType' crates/feder-runtime-server/tests/cases/inbox.rs

Repository: fedify-dev/feder

Length of output: 5166


🏁 Script executed:

#!/bin/bash
set -u

printf '%s\n' '--- crates/feder-runtime-server/src/inbox.rs line 330-370 ---'
sed -n '330,370p' crates/feder-runtime-server/src/inbox.rs

printf '%s\n' '--- all follow/accept test bodies around fixture usage ---'
sed -n '100,160p' crates/feder-runtime-server/tests/cases/inbox.rs
sed -n '240,340p' crates/feder-runtime-server/tests/cases/inbox.rs

Repository: fedify-dev/feder

Length of output: 7446


🌐 Web query:

JSON-LD 1.1 @contextobject type vocab context object IRI@vocab``

💡 Result:

In JSON-LD 1.1, @vocab is a keyword used within the @context object to define a default vocabulary for the document [1][2]. It simplifies the mapping of terms by establishing a base IRI for properties and types that do not have an explicit mapping [1][3]. Key technical details regarding @vocab: Function: When @vocab is defined in the @context, any term used as a key in a node object (or a value of @type) that is not already defined as a term, not an absolute IRI, and does not contain a colon (i.e., not a compact IRI) is expanded by appending it to the @vocab IRI [1][2]. Context Integration: The @vocab keyword is placed inside a @context block [3]. It cannot be used more than once within the same context object, but it can be changed or shadowed by defining a new @context later in the document or using scoped contexts [3][4]. Relationship to IRIs: - The value of @vocab must be an IRI, a compact IRI, a term, or null [5][6]. - Since JSON-LD 1.1, the value can also be a relative IRI reference, which is resolved against the base IRI (or the vocabulary mapping of the active context) [1]. Distinction from @context: - The @context object is the container used to define all mappings (terms, prefixes, keywords, etc.) [2][7]. - @vocab is a specific configuration key used inside that @context to automate the expansion of vocabulary terms, preventing the need to explicitly define every single property or type name if they all originate from the same source (e.g., schema.org) [3]. Special Usage in @type: In JSON-LD 1.1, you can also set the @type of a property to "@vocab". This indicates that the values associated with that property should be treated as vocabulary items and expanded using the current active @vocab setting, rather than as literal strings or IRIs [4][8].

Citations:


🌐 Web query:

Mastodon ActivityPub Actor actor.json @context includes "toot"

💡 Result:

Yes, Mastodon's ActivityPub Actor objects typically include a toot namespace definition in their @context to support Mastodon-specific extensions [1][2][3]. The @context field in a Mastodon Actor JSON-LD object is often an array that includes the standard ActivityStreams context, along with a nested object defining the toot prefix [4][5][3]. This definition maps the toot prefix to the Mastodon namespace URI [1][2]: { "@context": [ "https://www.w3.org/ns/activitystreams", { "toot": "http://joinmastodon.org/ns#",... } ] } Within this namespace, Mastodon defines several custom properties used for Actor objects and other ActivityPub entities, such as featured (for pinned toots), featuredTags, blurhash, and discoverable [1][2][4]. By including this mapping in the @context, Mastodon ensures that these extended properties are correctly interpreted by other Fediverse servers [1][2]. Depending on the specific implementation or object type, additional contexts (such as the security context for public keys) may also be included in the @context array [5][3].

Citations:


Preserve non-IRI JSON-LD context terms when deserializing remote actors.

Mastodon actors use "@context": ["https://www.w3.org/ns/activitystreams", {"toot": "http://joinmastodon.org/ns#", ...}]; the current untagged Context enum rejects the object entry before ActorDocument can be built, so Actor resolution can fail while resolving a federated actor. Add an ignored term/object variant or custom deserializer for the ignored JSON-LD context fields (@vocab, @base, @language, terms), or deserialize to serde_json::Value before mapping to Context.

🤖 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 `@crates/feder-vocab/src/lib.rs` around lines 180 - 209, Update the Context
deserialization used by ActorDocument so JSON-LD context arrays containing
object or other non-IRI entries are accepted and preserved or safely ignored
while IRI entries continue mapping to Context::Iri or Context::Iris. Extend
Context with an ignored term/object variant or implement a custom deserializer,
ensuring Mastodon-style contexts deserialize without rejecting the actor.

Comment on lines +228 to +243
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
pub enum CryptographicKeyType {
#[default]
CryptographicKey,
}

/// A public key published by an ActivityPub actor.
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct CryptographicKey {
pub id: Iri,
#[serde(rename = "type", default)]
pub kind: CryptographicKeyType,
pub owner: Iri,
#[serde(rename = "publicKeyPem")]
pub public_key_pem: String,
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Files:"
git ls-files | rg '(^|/)lib\.rs$' || true

echo
echo "Search CryptographicKey:"
rg -n "CryptographicKey|publicKey" crates/feder-vocab/src/lib.rs || true

echo
echo "Relevant lines:"
sed -n '210,255p' crates/feder-vocab/src/lib.rs | cat -n -v

echo
echo "Imports and trait derives context:"
sed -n '1,225p' crates/feder-vocab/src/lib.rs | cat -n -v

echo
echo "Find deserializers for similar types:"
rg -n "deserialize_with|deserialize\(|serde_str|enum .*Default|default =|key" crates/feder-vocab/src/lib.rs || true

Repository: fedify-dev/feder

Length of output: 11094


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Check workspace/crates around activitypub schema if present:"
git ls-files | rg 'activitypub|vocab|feder' || true

echo
echo "Cargo manifests with serde deps:"
rg -n 'serde ="|serde' cargo.toml crates -g 'Cargo.toml' || true

Repository: fedify-dev/feder

Length of output: 2342


🌐 Web query:

W3C ActivityPub Activity Streams 2.0 publicKey type CryptographicKey schema

💡 Result:

In the context of ActivityPub and Activity Streams 2.0, the publicKey property and the CryptographicKey type are core components used primarily for identifying and verifying actor public keys during HTTP Signature operations [1][2][3]. CryptographicKey Schema The CryptographicKey type is defined in the Linked Data Security Vocabulary (often associated with the URI https://w3id.org/security#Key) [4][5]. While implementations vary, a standard CryptographicKey object within an ActivityPub actor document typically includes the following properties [2][3][6]: - id: A unique URI for the key, often represented as a fragment identifier on the actor's profile (e.g., https://example.com/actor#main-key) [2][3]. - owner: The URI of the actor who owns or controls the key [2][3][4]. - publicKeyPem: A PEM-encoded string containing the public key material (e.g., RSA public key) [1][7][3]. Usage and Context - publicKey Property: Actor objects use the publicKey property to expose these key objects [1][8]. Historically, this property was used to attach a single key (or a list of keys) directly to an actor [9][3]. - HTTP Signatures: When an ActivityPub actor sends a signed request, the signature includes a keyId parameter. Receivers use this keyId to fetch the actor's object and locate the corresponding CryptographicKey object via the publicKey field [1][7][3]. Evolution and Deprecation The publicKey property is considered deprecated in newer versions of the Linked Data Security Vocabulary, which favor modern standards like Verifiable Credential Data Integrity [1][7][10]. Current best practices involve the following: - FEP-521a: This proposal (FEP-521a) advocates for representing public keys using the Multikey type and moving them into properties like assertionMethod [2][10]. - Multikey: The Multikey type replaces the older CryptographicKey structure, using a publicKeyMultibase property (encoded with base-58-btc) instead of publicKeyPem [10]. Implementers are increasingly moving toward these newer standards to support key rotation and multiple verification relationships, though support for the legacy publicKey/CryptographicKey model remains widespread across the fediverse for compatibility [1][10].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Deserialize attribute for CryptographicKey:"
rg -n -C2 '#\[serde\(\).*key.*\]|derive\(Deserialize|enum CryptographicKeyType|struct CryptographicKey' crates/feder-vocab/src/lib.rs || true

echo
echo "Cargo serde version:"
git ls-files | rg 'workspace\.toml|Cargo\.toml|^Cargo\.lock$' | xargs rg -n 'serde|serde_json' || true

echo
echo "Behavioral probe of derive Deserialize single-variant enum with missing/present type attribute:"
python3 - <<'PY'
import json, re
from pathlib import Path

p = Path("crates/feder-vocab/src/lib.rs")
text = p.read_text()

print("Contains #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] for CryptographicKeyType:",
      "#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]\npub enum CryptographicKeyType" in text)
print("CryptographicKeyType has #[default] on single variant:", re.search(r'pub enum CryptographicKeyType\r?\n\[\`#serde`\(untagged\)\]\r?\n\{\}\r?\n\}\\s*\}\\s*#\[\`#default`\]\s*CryptographicKey', text, re.S))
print("kind field has rename + default:", 'rename = "type", default' in text)

cases = {
    "missing_type": {"id":"i","type":"Key","publicKeyPem":"p"},
}
for name, j in cases.items():
    print(name, json.dumps(j))
PY

Repository: fedify-dev/feder

Length of output: 2389


Make CryptographicKey.type tolerant of unknown values.

default only covers a missing type; present values like "Key" or arrays fail deserialization even though kind has no behavior associated with it. Handle unrecognized type as the default CryptographicKey instead of rejecting the actor document.

🤖 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 `@crates/feder-vocab/src/lib.rs` around lines 228 - 243, Update
CryptographicKeyType deserialization so present unknown values, including
strings such as "Key" and arrays, resolve to the default CryptographicKey
variant instead of failing; preserve the existing default for missing type and
the current enum behavior for recognized values.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement actor resolution, HTTP Signatures, and outgoing delivery

1 participant