@fedify/fedify 1.6.0-dev.778 → 1.6.0-dev.790
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGES.md +17 -0
- package/CONTRIBUTING.md +7 -0
- package/README.md +1 -1
- package/SPONSORS.md +1 -1
- package/esm/deno.js +3 -1
- package/esm/deps/jsr.io/@std/crypto/1.0.4/timing_safe_equal.js +55 -0
- package/esm/federation/middleware.js +26 -1
- package/esm/federation/send.js +6 -7
- package/esm/runtime/docloader.js +4 -16
- package/esm/sig/http.js +528 -9
- package/esm/sig/key.js +4 -1
- package/esm/testing/fixtures/remote.domain/users/bob +20 -0
- package/esm/vocab/vocab.js +308 -176
- package/package.json +2 -1
- package/types/deno.d.ts +2 -0
- package/types/deps/jsr.io/@std/crypto/1.0.4/timing_safe_equal.d.ts +34 -0
- package/types/deps/jsr.io/@std/crypto/1.0.4/timing_safe_equal.d.ts.map +1 -0
- package/types/federation/middleware.d.ts +21 -3
- package/types/federation/middleware.d.ts.map +1 -1
- package/types/federation/send.d.ts +6 -0
- package/types/federation/send.d.ts.map +1 -1
- package/types/runtime/docloader.d.ts +17 -1
- package/types/runtime/docloader.d.ts.map +1 -1
- package/types/sig/http.d.ts +128 -0
- package/types/sig/http.d.ts.map +1 -1
- package/types/sig/key.d.ts.map +1 -1
- package/types/sig/mod.d.ts +1 -1
- package/types/sig/mod.d.ts.map +1 -1
- package/types/vocab/vocab.d.ts.map +1 -1
- package/esm/deps/jsr.io/@std/bytes/1.0.5/copy.js +0 -50
- package/esm/deps/jsr.io/@std/bytes/1.0.5/ends_with.js +0 -36
- package/esm/deps/jsr.io/@std/bytes/1.0.5/equals.js +0 -82
- package/esm/deps/jsr.io/@std/bytes/1.0.5/includes_needle.js +0 -42
- package/esm/deps/jsr.io/@std/bytes/1.0.5/index_of_needle.js +0 -68
- package/esm/deps/jsr.io/@std/bytes/1.0.5/last_index_of_needle.js +0 -65
- package/esm/deps/jsr.io/@std/bytes/1.0.5/mod.js +0 -34
- package/esm/deps/jsr.io/@std/bytes/1.0.5/repeat.js +0 -43
- package/esm/deps/jsr.io/@std/bytes/1.0.5/starts_with.js +0 -34
- package/types/deps/jsr.io/@std/bytes/1.0.5/copy.d.ts +0 -41
- package/types/deps/jsr.io/@std/bytes/1.0.5/copy.d.ts.map +0 -1
- package/types/deps/jsr.io/@std/bytes/1.0.5/ends_with.d.ts +0 -24
- package/types/deps/jsr.io/@std/bytes/1.0.5/ends_with.d.ts.map +0 -1
- package/types/deps/jsr.io/@std/bytes/1.0.5/equals.d.ts +0 -22
- package/types/deps/jsr.io/@std/bytes/1.0.5/equals.d.ts.map +0 -1
- package/types/deps/jsr.io/@std/bytes/1.0.5/includes_needle.d.ts +0 -38
- package/types/deps/jsr.io/@std/bytes/1.0.5/includes_needle.d.ts.map +0 -1
- package/types/deps/jsr.io/@std/bytes/1.0.5/index_of_needle.d.ts +0 -45
- package/types/deps/jsr.io/@std/bytes/1.0.5/index_of_needle.d.ts.map +0 -1
- package/types/deps/jsr.io/@std/bytes/1.0.5/last_index_of_needle.d.ts +0 -42
- package/types/deps/jsr.io/@std/bytes/1.0.5/last_index_of_needle.d.ts.map +0 -1
- package/types/deps/jsr.io/@std/bytes/1.0.5/mod.d.ts +0 -33
- package/types/deps/jsr.io/@std/bytes/1.0.5/mod.d.ts.map +0 -1
- package/types/deps/jsr.io/@std/bytes/1.0.5/repeat.d.ts +0 -33
- package/types/deps/jsr.io/@std/bytes/1.0.5/repeat.d.ts.map +0 -1
- package/types/deps/jsr.io/@std/bytes/1.0.5/starts_with.d.ts +0 -24
- package/types/deps/jsr.io/@std/bytes/1.0.5/starts_with.d.ts.map +0 -1
package/CHANGES.md
CHANGED
@@ -22,6 +22,23 @@ To be released.
|
|
22
22
|
|
23
23
|
- Added `Router.trailingSlashInsensitive` property.
|
24
24
|
|
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
|
25
42
|
[#227]: https://github.com/fedify-dev/fedify/issues/227
|
26
43
|
|
27
44
|
|
package/CONTRIBUTING.md
CHANGED
@@ -232,6 +232,13 @@ the following command:
|
|
232
232
|
deno task -f @fedify/fedify test
|
233
233
|
~~~~
|
234
234
|
|
235
|
+
Or you can use `--filter` option to run a specific test. For example, if you
|
236
|
+
want to run the `verifyRequest` test:
|
237
|
+
|
238
|
+
~~~~ bash
|
239
|
+
deno task -f @fedify/fedify test --filter verifyRequest
|
240
|
+
~~~~
|
241
|
+
|
235
242
|
If the tests pass, you should run `deno task test-all` command to test
|
236
243
|
the library with Deno, Node.js, and [Bun]:
|
237
244
|
|
package/README.md
CHANGED
@@ -105,7 +105,7 @@ financial contributors:[^2]
|
|
105
105
|
|
106
106
|
### Backers
|
107
107
|
|
108
|
-
Robin Riley, yamanoku,
|
108
|
+
Robin Riley, yamanoku, taye, Encyclia, okin, Andy Piper, box464, Evan Prodromou, Rafael Goulart, malte
|
109
109
|
|
110
110
|
### One-time donations
|
111
111
|
|
package/SPONSORS.md
CHANGED
@@ -26,7 +26,7 @@ Supporters
|
|
26
26
|
Backers
|
27
27
|
-------
|
28
28
|
|
29
|
-
Robin Riley, yamanoku,
|
29
|
+
Robin Riley, yamanoku, taye, Encyclia, okin, Andy Piper, box464, Evan Prodromou, Rafael Goulart, malte
|
30
30
|
|
31
31
|
One-time donations
|
32
32
|
------------------
|
package/esm/deno.js
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
export default {
|
2
2
|
"name": "@fedify/fedify",
|
3
|
-
"version": "1.6.0-dev.
|
3
|
+
"version": "1.6.0-dev.790+3f8325df",
|
4
4
|
"license": "MIT",
|
5
5
|
"exports": {
|
6
6
|
".": "./mod.ts",
|
@@ -29,6 +29,7 @@ export default {
|
|
29
29
|
"@std/async": "jsr:@std/async@^1.0.5",
|
30
30
|
"@std/bytes": "jsr:@std/bytes@^1.0.2",
|
31
31
|
"@std/collections": "jsr:@std/collections@^1.0.6",
|
32
|
+
"@std/crypto": "jsr:@std/crypto@^1.0.4",
|
32
33
|
"@std/encoding": "jsr:@std/encoding@1.0.7",
|
33
34
|
"@std/http": "jsr:@std/http@^1.0.6",
|
34
35
|
"@std/testing": "jsr:@std/testing@^0.224.0",
|
@@ -42,6 +43,7 @@ export default {
|
|
42
43
|
"mock_fetch": "jsr:@hongminhee/deno-mock-fetch@^0.3.2",
|
43
44
|
"multicodec": "npm:multicodec@^3.2.1",
|
44
45
|
"pkijs": "npm:pkijs@^3.2.4",
|
46
|
+
"structured-field-values": "npm:structured-field-values@^2.0.4",
|
45
47
|
"uri-template-router": "npm:uri-template-router@^0.0.17",
|
46
48
|
"url-template": "npm:url-template@^3.1.1"
|
47
49
|
},
|
@@ -0,0 +1,55 @@
|
|
1
|
+
// Copyright 2018-2025 the Deno authors. MIT license.
|
2
|
+
// This module is browser compatible.
|
3
|
+
function toDataView(value) {
|
4
|
+
if (value instanceof DataView) {
|
5
|
+
return value;
|
6
|
+
}
|
7
|
+
return ArrayBuffer.isView(value)
|
8
|
+
? new DataView(value.buffer, value.byteOffset, value.byteLength)
|
9
|
+
: new DataView(value);
|
10
|
+
}
|
11
|
+
/**
|
12
|
+
* When checking the values of cryptographic hashes are equal, default
|
13
|
+
* comparisons can be susceptible to timing based attacks, where attacker is
|
14
|
+
* able to find out information about the host system by repeatedly checking
|
15
|
+
* response times to equality comparisons of values.
|
16
|
+
*
|
17
|
+
* It is likely some form of timing safe equality will make its way to the
|
18
|
+
* WebCrypto standard (see:
|
19
|
+
* {@link https://github.com/w3c/webcrypto/issues/270 | w3c/webcrypto#270}), but until
|
20
|
+
* that time, `timingSafeEqual()` is provided:
|
21
|
+
*
|
22
|
+
* @example Usage
|
23
|
+
* ```ts
|
24
|
+
* import { timingSafeEqual } from "@std/crypto/timing-safe-equal";
|
25
|
+
* import { assert } from "@std/assert";
|
26
|
+
*
|
27
|
+
* const a = await crypto.subtle.digest(
|
28
|
+
* "SHA-384",
|
29
|
+
* new TextEncoder().encode("hello world"),
|
30
|
+
* );
|
31
|
+
* const b = await crypto.subtle.digest(
|
32
|
+
* "SHA-384",
|
33
|
+
* new TextEncoder().encode("hello world"),
|
34
|
+
* );
|
35
|
+
*
|
36
|
+
* assert(timingSafeEqual(a, b));
|
37
|
+
* ```
|
38
|
+
*
|
39
|
+
* @param a The first value to compare.
|
40
|
+
* @param b The second value to compare.
|
41
|
+
* @returns `true` if the values are equal, otherwise `false`.
|
42
|
+
*/
|
43
|
+
export function timingSafeEqual(a, b) {
|
44
|
+
if (a.byteLength !== b.byteLength)
|
45
|
+
return false;
|
46
|
+
const dataViewA = toDataView(a);
|
47
|
+
const dataViewB = toDataView(b);
|
48
|
+
const length = a.byteLength;
|
49
|
+
let out = 0;
|
50
|
+
let i = -1;
|
51
|
+
while (++i < length) {
|
52
|
+
out |= dataViewA.getUint8(i) ^ dataViewB.getUint8(i);
|
53
|
+
}
|
54
|
+
return out === 0;
|
55
|
+
}
|
@@ -7,7 +7,7 @@ import metadata from "../deno.js";
|
|
7
7
|
import { getNodeInfo } from "../nodeinfo/client.js";
|
8
8
|
import { handleNodeInfo, handleNodeInfoJrd } from "../nodeinfo/handler.js";
|
9
9
|
import { getAuthenticatedDocumentLoader, getDocumentLoader, kvCache, } from "../runtime/docloader.js";
|
10
|
-
import { verifyRequest } from "../sig/http.js";
|
10
|
+
import { verifyRequest, } from "../sig/http.js";
|
11
11
|
import { exportJwk, importJwk, validateCryptoKey } from "../sig/key.js";
|
12
12
|
import { hasSignature, signJsonLd } from "../sig/ld.js";
|
13
13
|
import { getKeyOwner } from "../sig/owner.js";
|
@@ -66,6 +66,7 @@ export class FederationImpl extends FederationBuilderImpl {
|
|
66
66
|
activityIdempotence: ["_fedify", "activityIdempotence"],
|
67
67
|
remoteDocument: ["_fedify", "remoteDocument"],
|
68
68
|
publicKey: ["_fedify", "publicKey"],
|
69
|
+
httpMessageSignaturesSpec: ["_fedify", "httpMessageSignaturesSpec"],
|
69
70
|
},
|
70
71
|
...(options.kvPrefixes ?? {}),
|
71
72
|
};
|
@@ -179,6 +180,8 @@ export class FederationImpl extends FederationBuilderImpl {
|
|
179
180
|
((identity) => getAuthenticatedDocumentLoader(identity, {
|
180
181
|
allowPrivateAddress,
|
181
182
|
userAgent,
|
183
|
+
specDeterminer: new KvSpecDeterminer(this.kv, this.kvPrefixes.httpMessageSignaturesSpec),
|
184
|
+
tracerProvider: this.tracerProvider,
|
182
185
|
}));
|
183
186
|
this.userAgent = userAgent;
|
184
187
|
this.onOutboxError = options.onOutboxError;
|
@@ -372,6 +375,7 @@ export class FederationImpl extends FederationBuilderImpl {
|
|
372
375
|
inbox: new URL(message.inbox),
|
373
376
|
sharedInbox: message.sharedInbox,
|
374
377
|
headers: new Headers(message.headers),
|
378
|
+
specDeterminer: new KvSpecDeterminer(this.kv, this.kvPrefixes.httpMessageSignaturesSpec),
|
375
379
|
tracerProvider: this.tracerProvider,
|
376
380
|
});
|
377
381
|
}
|
@@ -674,6 +678,7 @@ export class FederationImpl extends FederationBuilderImpl {
|
|
674
678
|
headers: collectionSync == null ? undefined : new Headers({
|
675
679
|
"Collection-Synchronization": await buildCollectionSynchronizationHeader(collectionSync, inboxes[inbox].actorIds),
|
676
680
|
}),
|
681
|
+
specDeterminer: new KvSpecDeterminer(this.kv, this.kvPrefixes.httpMessageSignaturesSpec),
|
677
682
|
tracerProvider: this.tracerProvider,
|
678
683
|
}));
|
679
684
|
}
|
@@ -1939,6 +1944,7 @@ export class InboxContextImpl extends ContextImpl {
|
|
1939
1944
|
inbox: new URL(inbox),
|
1940
1945
|
sharedInbox: inboxes[inbox].sharedInbox,
|
1941
1946
|
tracerProvider: this.tracerProvider,
|
1947
|
+
specDeterminer: new KvSpecDeterminer(this.federation.kv, this.federation.kvPrefixes.httpMessageSignaturesSpec),
|
1942
1948
|
}));
|
1943
1949
|
}
|
1944
1950
|
await Promise.all(promises);
|
@@ -1997,6 +2003,25 @@ export class InboxContextImpl extends ContextImpl {
|
|
1997
2003
|
}
|
1998
2004
|
}
|
1999
2005
|
}
|
2006
|
+
export class KvSpecDeterminer {
|
2007
|
+
kv;
|
2008
|
+
prefix;
|
2009
|
+
defaultSpec;
|
2010
|
+
constructor(kv, prefix, defaultSpec = "rfc9421") {
|
2011
|
+
this.kv = kv;
|
2012
|
+
this.prefix = prefix;
|
2013
|
+
this.defaultSpec = defaultSpec;
|
2014
|
+
}
|
2015
|
+
async determineSpec(origin) {
|
2016
|
+
return await this.kv.get([
|
2017
|
+
...this.prefix,
|
2018
|
+
origin,
|
2019
|
+
]) ?? this.defaultSpec;
|
2020
|
+
}
|
2021
|
+
async rememberSpec(origin, spec) {
|
2022
|
+
await this.kv.set([...this.prefix, origin], spec);
|
2023
|
+
}
|
2024
|
+
}
|
2000
2025
|
function notFound(_request) {
|
2001
2026
|
return new Response("Not Found", { status: 404 });
|
2002
2027
|
}
|
package/esm/federation/send.js
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
import { getLogger } from "@logtape/logtape";
|
2
2
|
import { SpanKind, SpanStatusCode, trace, } from "@opentelemetry/api";
|
3
3
|
import metadata from "../deno.js";
|
4
|
-
import {
|
4
|
+
import { doubleKnock, } from "../sig/http.js";
|
5
5
|
/**
|
6
6
|
* Extracts the inbox URLs from recipients.
|
7
7
|
* @param parameters The parameters to extract the inboxes.
|
@@ -65,11 +65,11 @@ export function sendActivity(options) {
|
|
65
65
|
}
|
66
66
|
});
|
67
67
|
}
|
68
|
-
async function sendActivityInternal({ activity, activityId, keys, inbox, headers, tracerProvider, }) {
|
68
|
+
async function sendActivityInternal({ activity, activityId, keys, inbox, headers, specDeterminer, tracerProvider, }) {
|
69
69
|
const logger = getLogger(["fedify", "federation", "outbox"]);
|
70
70
|
headers = new Headers(headers);
|
71
71
|
headers.set("Content-Type", "application/activity+json");
|
72
|
-
|
72
|
+
const request = new Request(inbox, {
|
73
73
|
method: "POST",
|
74
74
|
headers,
|
75
75
|
body: JSON.stringify(activity),
|
@@ -93,12 +93,11 @@ async function sendActivityInternal({ activity, activityId, keys, inbox, headers
|
|
93
93
|
})),
|
94
94
|
});
|
95
95
|
}
|
96
|
-
else {
|
97
|
-
request = await signRequest(request, rsaKey.privateKey, rsaKey.keyId, { tracerProvider });
|
98
|
-
}
|
99
96
|
let response;
|
100
97
|
try {
|
101
|
-
response =
|
98
|
+
response = rsaKey == null
|
99
|
+
? await fetch(request)
|
100
|
+
: await doubleKnock(request, rsaKey, { tracerProvider, specDeterminer });
|
102
101
|
}
|
103
102
|
catch (error) {
|
104
103
|
logger.error("Failed to send activity {activityId} to {inbox}:\n{error}", {
|
package/esm/runtime/docloader.js
CHANGED
@@ -4,7 +4,7 @@ import { HTTPHeaderLink } from "@hugoalh/http-header-link";
|
|
4
4
|
import { getLogger } from "@logtape/logtape";
|
5
5
|
import process from "node:process";
|
6
6
|
import metadata from "../deno.js";
|
7
|
-
import {
|
7
|
+
import { doubleKnock, } from "../sig/http.js";
|
8
8
|
import { validateCryptoKey } from "../sig/key.js";
|
9
9
|
import preloadedContexts from "./contexts.js";
|
10
10
|
import { UrlError, validatePublicUrl } from "./url.js";
|
@@ -238,7 +238,7 @@ export function fetchDocumentLoader(url, allowPrivateAddress = false) {
|
|
238
238
|
* @throws {TypeError} If the key is invalid or unsupported.
|
239
239
|
* @since 0.4.0
|
240
240
|
*/
|
241
|
-
export function getAuthenticatedDocumentLoader(identity, { allowPrivateAddress, userAgent } = {}) {
|
241
|
+
export function getAuthenticatedDocumentLoader(identity, { allowPrivateAddress, userAgent, specDeterminer, tracerProvider } = {}) {
|
242
242
|
validateCryptoKey(identity.privateKey);
|
243
243
|
async function load(url) {
|
244
244
|
if (!allowPrivateAddress) {
|
@@ -252,20 +252,8 @@ export function getAuthenticatedDocumentLoader(identity, { allowPrivateAddress,
|
|
252
252
|
throw error;
|
253
253
|
}
|
254
254
|
}
|
255
|
-
|
256
|
-
|
257
|
-
logRequest(request);
|
258
|
-
const response = await fetch(request, {
|
259
|
-
// Since Bun has a bug that ignores the `Request.redirect` option,
|
260
|
-
// to work around it we specify `redirect: "manual"` here too:
|
261
|
-
// https://github.com/oven-sh/bun/issues/10754
|
262
|
-
redirect: "manual",
|
263
|
-
});
|
264
|
-
// Follow redirects manually to get the final URL:
|
265
|
-
if (response.status >= 300 && response.status < 400 &&
|
266
|
-
response.headers.has("Location")) {
|
267
|
-
return load(response.headers.get("Location"));
|
268
|
-
}
|
255
|
+
const originalRequest = createRequest(url, { userAgent });
|
256
|
+
const response = await doubleKnock(originalRequest, identity, { specDeterminer, log: logRequest, tracerProvider });
|
269
257
|
return getRemoteDocument(url, response, load);
|
270
258
|
}
|
271
259
|
return load;
|