Skip to content

Commit 3f8325d

Browse files
committed
Merge branch 'rfc9421'
2 parents 09b29f9 + 7939141 commit 3f8325d

File tree

23 files changed

+2801
-61
lines changed

23 files changed

+2801
-61
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,5 @@ deno.lock
33
repomix-output.xml
44
t.ts
55
t2.ts
6+
7+
**/.claude/settings.local.json

CHANGES.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,23 @@ To be released.
2222

2323
- Added `Router.trailingSlashInsensitive` property.
2424

25+
- Implemented HTTP Message Signatures ([RFC 9421]) with double-knocking.
26+
Currently, it only works with RSA-PKCS#1-v1.5. [[#208]]
27+
28+
- Added `HttpMessageSignaturesSpec` type.
29+
- Added `SignRequestOptions.spec` option.
30+
- Added `SignRequestOptions.currentTime` option.
31+
- Added `VerifyRequestOptions.spec` option.
32+
- Added `GetAuthenticatedDocumentLoaderOptions.specDeterminer` option.
33+
- Added `GetAuthenticatedDocumentLoaderOptions.traceProvider` option.
34+
- Added `HttpMessageSignaturesSpecDeterminer` interface.
35+
- Added `--first-knock` option to `fedify lookup` command.
36+
37+
- The `exportJwk()` function now populates the `alg` property of a returned
38+
`JsonWebKey` object with `"Ed25519"` if the input key is an Ed25519 key.
39+
40+
[RFC 9421]: https://www.rfc-editor.org/rfc/rfc9421
41+
[#208]: https://github.com/fedify-dev/fedify/issues/208
2542
[#227]: https://github.com/fedify-dev/fedify/issues/227
2643

2744

cli/lookup.ts

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { colors } from "@cliffy/ansi";
2-
import { Command } from "@cliffy/command";
2+
import { Command, EnumType } from "@cliffy/command";
33
import {
44
Application,
55
Collection,
@@ -22,14 +22,23 @@ import { printJson } from "./utils.ts";
2222

2323
const logger = getLogger(["fedify", "cli", "lookup"]);
2424

25+
const sigSpec = new EnumType(["draft-cavage-http-signatures-12", "rfc9421"]);
26+
2527
export const command = new Command()
28+
.type("sig-spec", sigSpec)
2629
.arguments("<...urls:string>")
2730
.description(
2831
"Lookup an Activity Streams object by URL or the actor handle. " +
2932
"The argument can be either a URL or an actor handle " +
3033
"(e.g., @username@domain), and it can be multiple.",
3134
)
3235
.option("-a, --authorized-fetch", "Sign the request with an one-time key.")
36+
.option(
37+
"--first-knock <spec:sig-spec>",
38+
"The first-knock spec for -a/--authorized-fetch. It is used for " +
39+
"the double-knocking technique.",
40+
{ depends: ["authorized-fetch"], default: "rfc9421" },
41+
)
3342
.option(
3443
"-t, --traverse",
3544
"Traverse the given collection to fetch all items. If it is turned on, " +
@@ -118,10 +127,21 @@ export const command = new Command()
118127
{ contextLoader },
119128
);
120129
});
121-
authLoader = getAuthenticatedDocumentLoader({
122-
keyId: new URL("#main-key", server.url),
123-
privateKey: key.privateKey,
124-
});
130+
authLoader = getAuthenticatedDocumentLoader(
131+
{
132+
keyId: new URL("#main-key", server.url),
133+
privateKey: key.privateKey,
134+
},
135+
{
136+
specDeterminer: {
137+
determineSpec() {
138+
return options.firstKnock;
139+
},
140+
rememberSpec() {
141+
},
142+
},
143+
},
144+
);
125145
}
126146
spinner.text = `Looking up the ${
127147
options.traverse ? "collection" : urls.length > 1 ? "objects" : "object"
@@ -255,3 +275,5 @@ export const command = new Command()
255275
Deno.exit(1);
256276
}
257277
});
278+
279+
// cSpell: ignore sigspec

cli/mod.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Command, CompletionsCommand, HelpCommand } from "@cliffy/command";
22
import { configure, getConsoleSink, getFileSink } from "@logtape/logtape";
3+
import { AsyncLocalStorage } from "node:async_hooks";
34
import { DEFAULT_CACHE_DIR, setCacheDir } from "./cache.ts";
45
import metadata from "./deno.json" with { type: "json" };
56
import { command as inbox } from "./inbox.tsx";
@@ -46,6 +47,7 @@ const command = new Command()
4647
},
4748
],
4849
reset: true,
50+
contextLocalStorage: new AsyncLocalStorage(),
4951
});
5052
},
5153
})

cspell.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"dereferenceable",
2424
"discoverability",
2525
"docloader",
26+
"draft-cavage",
2627
"eddsa",
2728
"fanout",
2829
"federatable",

deno.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"check": "deno task -f @fedify/fedify check && deno task -f @fedify/cli check && deno task -f @fedify/blog check && deno task -f @fedify/hono-sample check",
2525
"test-all": "deno task -f @fedify/fedify test-all && deno task -f @fedify/cli check && deno task -f @fedify/blog check && deno task -f @fedify/hono-sample check",
2626
"publish": "deno task -f @fedify/fedify publish && deno task -f @fedify/cli publish",
27+
"cli": "deno task -f @fedify/cli run",
2728
"hooks:install": "deno run --allow-read=deno.json,.git/hooks/ --allow-write=.git/hooks/ jsr:@hongminhee/deno-task-hooks",
2829
"hooks:pre-commit": {
2930
"dependencies": [

docs/cli.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -727,6 +727,36 @@ Person {
727727
}
728728
~~~~
729729

730+
### `--first-knock`: First-knock spec for `-a`/`--authorized-fetch`
731+
732+
*This option is available since Fedify 1.6.0.*
733+
734+
The `--first-knock` option is used to specify which HTTP Signatures spec to
735+
try first when using the `-a`/`--authorized-fetch` option. The ActivityPub
736+
ecosystem currently uses different versions of HTTP Signatures specifications,
737+
and the [double-knocking] technique (trying one version, then falling back to
738+
another if rejected) allows for better compatibility across servers.
739+
740+
Available options are:
741+
742+
`draft-cavage-http-signatures-12`
743+
: [HTTP Signatures], which is obsolete but still widely adopted in
744+
the fediverse as of May 2025.
745+
746+
`rfc9421` (default)
747+
: [RFC 9421]: HTTP Message Signatures, which is the final revision of
748+
the specification and is recommended, but not yet widely adopted
749+
in the fediverse as of May 2025.
750+
751+
If the first signature attempt fails, Fedify will automatically try the other
752+
specification format, implementing the [double-knocking] technique described in
753+
the [ActivityPub HTTP Signatures] specification.
754+
755+
[double-knocking]: https://swicg.github.io/activitypub-http-signature/#how-to-upgrade-supported-versions
756+
[HTTP Signatures]: https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures-12
757+
[RFC 9421]: https://www.rfc-editor.org/rfc/rfc9421
758+
[ActivityPub HTTP Signatures]: https://swicg.github.io/activitypub-http-signature/
759+
730760
### `-u`/`--user-agent`: Custom `User-Agent` header
731761

732762
*This option is available since Fedify 1.3.0.*

docs/manual/federation.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,16 @@ that the `Federation` object uses:
8383
The key prefix used for caching public keys. `["_fedify", "publicKey"]`
8484
by default.
8585

86+
`~FederationKvPrefixes.httpMessageSignaturesSpec`
87+
: *This API is available since Fedify 1.6.0.*
88+
89+
The key prefix used for caching HTTP Message Signatures spec. The cached
90+
spec is used to reduce the number of attempts to make signed requests
91+
([double-knocking] technique).
92+
`["_fedify", "httpMessageSignaturesSpec"]` by default.
93+
94+
[double-knocking]: https://swicg.github.io/activitypub-http-signature/#how-to-upgrade-supported-versions
95+
8696
### `queue`
8797

8898
*This API is available since Fedify 0.5.0.*

docs/manual/inbox.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,28 @@ handle incoming activities from other actors.
1515
[inbox]: https://www.w3.org/TR/activitypub/#inbox
1616

1717

18+
Signature verification
19+
----------------------
20+
21+
The inbox listeners automatically verify the signature of the incoming
22+
activities with various specifications, such as:
23+
24+
- Draft cavage [HTTP Signatures]
25+
- HTTP Message Signatures ([RFC 9421])
26+
- [Linked Data Signatures]
27+
- Object Integrity Proofs ([FEP-8b32])
28+
29+
You don't mind about the signature verification at all—unsigned activities and
30+
invalid signatures are silently ignored. If you want to see why some activities
31+
are ignored, you can turn on [logging](./log.md) for `["fedify", "sig"]`
32+
category.
33+
34+
[HTTP Signatures]: https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures-12
35+
[RFC 9421]: https://www.rfc-editor.org/rfc/rfc9421
36+
[Linked Data Signatures]: https://web.archive.org/web/20170923124140/https://w3c-dvcg.github.io/ld-signatures/
37+
[FEP-8b32]: https://w3id.org/fep/8b32
38+
39+
1840
Registering an inbox listener
1941
-----------------------------
2042

@@ -555,3 +577,5 @@ for await (const item of context.traverseCollection(collection)) {
555577
> - The `Activity` is dereferenceable by its `~Object.id` and
556578
> the dereferenced object has an actor that belongs to the same origin
557579
> as the `Activity` object.
580+
581+
<!-- cSpell: ignore cavage -->

docs/manual/send.md

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -745,16 +745,66 @@ const federation = createFederation({
745745
HTTP Signatures
746746
---------------
747747

748-
[HTTP Signatures] is a de facto standard for signing ActivityPub activities.
749-
It is widely used in the fediverse to verify the sender's identity and
750-
the integrity of the activity.
748+
Draft cavage [HTTP Signatures] is a de facto standard for signing ActivityPub
749+
activities. Although it is not a finalized specification, it is still widely
750+
used in the fediverse to verify the sender's identity and the integrity of
751+
the activity.
751752

752753
Fedify automatically signs activities with the sender's private key if
753754
the [actor keys dispatcher is set](./actor.md#public-keys-of-an-actor) and
754755
the actor has any RSA-PKCS#1-v1.5 key pair. If there are multiple key pairs,
755756
Fedify selects the first RSA-PKCS#1-v1.5 key pair among them.
756757

757-
[HTTP Signatures]: https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures
758+
[HTTP Signatures]: https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures-12
759+
760+
761+
HTTP Message Signatures
762+
-----------------------
763+
764+
*This API is available since Fedify 1.6.0.*
765+
766+
[RFC 9421], also known as *HTTP Message Signatures*, is the final revision of
767+
the HTTP Signatures specification. Although it is the official standard,
768+
it is not widely used in the fediverse yet. As of May 2025, major ActivityPub
769+
implementations, such as Mastodon, et al., still rely on the draft cavage
770+
version of HTTP Signatures for signing portable activities.
771+
772+
Fedify automatically signs activities with the sender's private key if
773+
the [actor keys dispatcher is set](./actor.md#public-keys-of-an-actor) and
774+
the actor has any RSA-PKCS#1-v1.5 key pair. If there are multiple key pairs,
775+
Fedify selects the first RSA-PKCS#1-v1.5 key pair among them.
776+
777+
> [!NOTE]
778+
> Although HTTP Message Signatures support other than RSA-PKCS#1-v1.5,
779+
> Fedify currently supports only RSA-PKCS#1-v1.5 key pairs for generating
780+
> HTTP Message Signatures. This limitation will be lifted in the future
781+
> releases.
782+
783+
[RFC 9421]: https://www.rfc-editor.org/rfc/rfc9421
784+
785+
786+
Double-knocking HTTP Signatures
787+
-------------------------------
788+
789+
*This API is available since Fedify 1.6.0.*
790+
791+
As you read above, there are two revisions of HTTP Signatures:
792+
the [draft cavage][HTTP Signatures] version and the [RFC 9421] version.
793+
The draft cavage version is declared as obsolete, but it is still widely used
794+
in the fediverse, and many ActivityPub implementations still rely on it.
795+
On the other hand, the RFC 9421 version is the official standard, but
796+
it is not widely used yet.
797+
798+
To support both versions of HTTP Signatures, Fedify uses the [double-knocking]
799+
mechanism: trying one version, then falling back to another if rejected.
800+
If it's the first encounter with the recipient server, Fedify tries
801+
the RFC 9421 version first, and if it fails, it falls back to the draft
802+
cavage version. If the recipient server accepts the RFC 9421 version,
803+
Fedify remembers it and uses the RFC 9421 version for the next time.
804+
If the recipient server rejects the RFC 9421 version, Fedify falls back
805+
to the draft cavage version and remembers it for the next time.
806+
807+
[double-knocking]: https://swicg.github.io/activitypub-http-signature/#how-to-upgrade-supported-versions
758808

759809

760810
Linked Data Signatures
@@ -914,3 +964,5 @@ new Follow({
914964
~~~~
915965

916966
[Threads]: https://www.threads.net/
967+
968+
<!-- cSpell: ignore cavage -->

0 commit comments

Comments
 (0)