@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.
Files changed (56) hide show
  1. package/CHANGES.md +17 -0
  2. package/CONTRIBUTING.md +7 -0
  3. package/README.md +1 -1
  4. package/SPONSORS.md +1 -1
  5. package/esm/deno.js +3 -1
  6. package/esm/deps/jsr.io/@std/crypto/1.0.4/timing_safe_equal.js +55 -0
  7. package/esm/federation/middleware.js +26 -1
  8. package/esm/federation/send.js +6 -7
  9. package/esm/runtime/docloader.js +4 -16
  10. package/esm/sig/http.js +528 -9
  11. package/esm/sig/key.js +4 -1
  12. package/esm/testing/fixtures/remote.domain/users/bob +20 -0
  13. package/esm/vocab/vocab.js +308 -176
  14. package/package.json +2 -1
  15. package/types/deno.d.ts +2 -0
  16. package/types/deps/jsr.io/@std/crypto/1.0.4/timing_safe_equal.d.ts +34 -0
  17. package/types/deps/jsr.io/@std/crypto/1.0.4/timing_safe_equal.d.ts.map +1 -0
  18. package/types/federation/middleware.d.ts +21 -3
  19. package/types/federation/middleware.d.ts.map +1 -1
  20. package/types/federation/send.d.ts +6 -0
  21. package/types/federation/send.d.ts.map +1 -1
  22. package/types/runtime/docloader.d.ts +17 -1
  23. package/types/runtime/docloader.d.ts.map +1 -1
  24. package/types/sig/http.d.ts +128 -0
  25. package/types/sig/http.d.ts.map +1 -1
  26. package/types/sig/key.d.ts.map +1 -1
  27. package/types/sig/mod.d.ts +1 -1
  28. package/types/sig/mod.d.ts.map +1 -1
  29. package/types/vocab/vocab.d.ts.map +1 -1
  30. package/esm/deps/jsr.io/@std/bytes/1.0.5/copy.js +0 -50
  31. package/esm/deps/jsr.io/@std/bytes/1.0.5/ends_with.js +0 -36
  32. package/esm/deps/jsr.io/@std/bytes/1.0.5/equals.js +0 -82
  33. package/esm/deps/jsr.io/@std/bytes/1.0.5/includes_needle.js +0 -42
  34. package/esm/deps/jsr.io/@std/bytes/1.0.5/index_of_needle.js +0 -68
  35. package/esm/deps/jsr.io/@std/bytes/1.0.5/last_index_of_needle.js +0 -65
  36. package/esm/deps/jsr.io/@std/bytes/1.0.5/mod.js +0 -34
  37. package/esm/deps/jsr.io/@std/bytes/1.0.5/repeat.js +0 -43
  38. package/esm/deps/jsr.io/@std/bytes/1.0.5/starts_with.js +0 -34
  39. package/types/deps/jsr.io/@std/bytes/1.0.5/copy.d.ts +0 -41
  40. package/types/deps/jsr.io/@std/bytes/1.0.5/copy.d.ts.map +0 -1
  41. package/types/deps/jsr.io/@std/bytes/1.0.5/ends_with.d.ts +0 -24
  42. package/types/deps/jsr.io/@std/bytes/1.0.5/ends_with.d.ts.map +0 -1
  43. package/types/deps/jsr.io/@std/bytes/1.0.5/equals.d.ts +0 -22
  44. package/types/deps/jsr.io/@std/bytes/1.0.5/equals.d.ts.map +0 -1
  45. package/types/deps/jsr.io/@std/bytes/1.0.5/includes_needle.d.ts +0 -38
  46. package/types/deps/jsr.io/@std/bytes/1.0.5/includes_needle.d.ts.map +0 -1
  47. package/types/deps/jsr.io/@std/bytes/1.0.5/index_of_needle.d.ts +0 -45
  48. package/types/deps/jsr.io/@std/bytes/1.0.5/index_of_needle.d.ts.map +0 -1
  49. package/types/deps/jsr.io/@std/bytes/1.0.5/last_index_of_needle.d.ts +0 -42
  50. package/types/deps/jsr.io/@std/bytes/1.0.5/last_index_of_needle.d.ts.map +0 -1
  51. package/types/deps/jsr.io/@std/bytes/1.0.5/mod.d.ts +0 -33
  52. package/types/deps/jsr.io/@std/bytes/1.0.5/mod.d.ts.map +0 -1
  53. package/types/deps/jsr.io/@std/bytes/1.0.5/repeat.d.ts +0 -33
  54. package/types/deps/jsr.io/@std/bytes/1.0.5/repeat.d.ts.map +0 -1
  55. package/types/deps/jsr.io/@std/bytes/1.0.5/starts_with.d.ts +0 -24
  56. 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, Encyclia, taye, okin, Andy Piper, box464, Evan Prodromou, Rafael Goulart, malte
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, Encyclia, taye, okin, Andy Piper, box464, Evan Prodromou, Rafael Goulart, malte
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.778+c3a73aa9",
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
  }
@@ -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 { signRequest } from "../sig/http.js";
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
- let request = new Request(inbox, {
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 = await fetch(request);
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}", {
@@ -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 { signRequest } from "../sig/http.js";
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
- let request = createRequest(url, { userAgent });
256
- request = await signRequest(request, identity.privateKey, identity.keyId);
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;