@fedify/fedify 0.15.0 → 1.0.0-dev.388

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 (51) hide show
  1. package/CHANGES.md +33 -0
  2. package/FEDERATION.md +2 -0
  3. package/esm/federation/handler.js +44 -17
  4. package/esm/federation/send.js +31 -13
  5. package/esm/runtime/contexts.js +152 -0
  6. package/esm/sig/http.js +11 -1
  7. package/esm/sig/ld.js +203 -0
  8. package/esm/sig/mod.js +1 -0
  9. package/esm/testing/fixtures/activitypub.academy/users/brauca_darradiul +83 -0
  10. package/esm/testing/fixtures/w3id.org/identity/v1 +152 -0
  11. package/esm/vocab/accept.yaml +1 -0
  12. package/esm/vocab/activity.yaml +1 -0
  13. package/esm/vocab/add.yaml +1 -0
  14. package/esm/vocab/announce.yaml +1 -0
  15. package/esm/vocab/arrive.yaml +1 -0
  16. package/esm/vocab/block.yaml +1 -0
  17. package/esm/vocab/create.yaml +1 -0
  18. package/esm/vocab/delete.yaml +1 -0
  19. package/esm/vocab/dislike.yaml +1 -0
  20. package/esm/vocab/flag.yaml +1 -0
  21. package/esm/vocab/follow.yaml +1 -0
  22. package/esm/vocab/ignore.yaml +1 -0
  23. package/esm/vocab/intransitiveactivity.yaml +1 -0
  24. package/esm/vocab/invite.yaml +1 -0
  25. package/esm/vocab/join.yaml +1 -0
  26. package/esm/vocab/leave.yaml +1 -0
  27. package/esm/vocab/like.yaml +1 -0
  28. package/esm/vocab/listen.yaml +1 -0
  29. package/esm/vocab/move.yaml +1 -0
  30. package/esm/vocab/offer.yaml +1 -0
  31. package/esm/vocab/question.yaml +1 -0
  32. package/esm/vocab/read.yaml +1 -0
  33. package/esm/vocab/reject.yaml +1 -0
  34. package/esm/vocab/remove.yaml +1 -0
  35. package/esm/vocab/tentativeaccept.yaml +1 -0
  36. package/esm/vocab/tentativereject.yaml +1 -0
  37. package/esm/vocab/travel.yaml +1 -0
  38. package/esm/vocab/undo.yaml +1 -0
  39. package/esm/vocab/update.yaml +1 -0
  40. package/esm/vocab/view.yaml +1 -0
  41. package/esm/vocab/vocab.js +59 -0
  42. package/package.json +1 -1
  43. package/types/federation/handler.d.ts.map +1 -1
  44. package/types/federation/send.d.ts.map +1 -1
  45. package/types/runtime/contexts.d.ts.map +1 -1
  46. package/types/sig/http.d.ts.map +1 -1
  47. package/types/sig/ld.d.ts +129 -0
  48. package/types/sig/ld.d.ts.map +1 -0
  49. package/types/sig/mod.d.ts +1 -0
  50. package/types/sig/mod.d.ts.map +1 -1
  51. package/types/vocab/vocab.d.ts.map +1 -1
package/CHANGES.md CHANGED
@@ -3,6 +3,39 @@
3
3
  Fedify changelog
4
4
  ================
5
5
 
6
+ Version 1.0.0
7
+ --------------
8
+
9
+ To be released.
10
+
11
+ - Fedify now supports [Linked Data Signatures], which is outdated but still
12
+ widely used in the fediverse.
13
+
14
+ - A `Federation` object became to verify an activity's Linked Data
15
+ Signatures if it has one. If Linked Data Signatures are verified,
16
+ Object Integrity Proofs and HTTP Signatures are not verified.
17
+ - `Context.sendActivity()` method became to sign an activity with Linked
18
+ Data Signatures if there is at least one RSA-PKCS#1-v1.5 key pair.
19
+ - Added `Signature` interface.
20
+ - Added `signJsonLd()` function.
21
+ - Added `SignJsonLdOptions` interface.
22
+ - Added `createSignature()` function.
23
+ - Added `CreateSignatureOptions` interface.
24
+ - Added `verifyJsonLd()` function.
25
+ - Added `VerifyJsonLdOptions` interface.
26
+ - Added `verifySignature()` function.
27
+ - Added `VerifySignatureOptions` interface.
28
+ - Added `attachSignature()` function.
29
+ - Added `detachSignature()` function.
30
+
31
+ - Added more log messages using the [LogTape] library. Currently the below
32
+ logger categories are used:
33
+
34
+ - `["fedify", "sig", "ld"]`
35
+
36
+ [Linked Data Signatures]: https://web.archive.org/web/20170923124140/https://w3c-dvcg.github.io/ld-signatures/
37
+
38
+
6
39
  Version 0.15.0
7
40
  --------------
8
41
 
package/FEDERATION.md CHANGED
@@ -9,11 +9,13 @@ Supported federation protocols and standards
9
9
  - [ActivityPub] (S2S)
10
10
  - [WebFinger]
11
11
  - [HTTP Signatures]
12
+ - [Linked Data Signatures]
12
13
  - [NodeInfo]
13
14
 
14
15
  [ActivityPub]: https://www.w3.org/TR/activitypub/
15
16
  [WebFinger]: https://datatracker.ietf.org/doc/html/rfc7033
16
17
  [HTTP Signatures]: https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures
18
+ [Linked Data Signatures]: https://web.archive.org/web/20170923124140/https://w3c-dvcg.github.io/ld-signatures/
17
19
  [NodeInfo]: https://nodeinfo.diaspora.software/
18
20
 
19
21
 
@@ -2,6 +2,7 @@ import * as dntShim from "../_dnt.shims.js";
2
2
  import { getLogger } from "@logtape/logtape";
3
3
  import { accepts } from "../deps/jsr.io/@std/http/0.224.5/negotiation.js";
4
4
  import { verifyRequest } from "../sig/http.js";
5
+ import { detachSignature, verifyJsonLd } from "../sig/ld.js";
5
6
  import { doesActorOwnKey } from "../sig/owner.js";
6
7
  import { verifyObject } from "../sig/proof.js";
7
8
  import { Activity, CryptographicKey, Link, Multikey, Object, OrderedCollection, OrderedCollectionPage, } from "../vocab/vocab.js";
@@ -229,26 +230,49 @@ export async function handleInbox(request, { handle, context, kv, kvPrefixes, qu
229
230
  await kv.set([...kvPrefixes.publicKey, keyId.href], serialized);
230
231
  },
231
232
  };
232
- let activity;
233
- try {
234
- activity = await verifyObject(Activity, json, {
235
- contextLoader: context.contextLoader,
236
- documentLoader: context.documentLoader,
237
- keyCache,
238
- });
233
+ const ldSigVerified = await verifyJsonLd(json, {
234
+ contextLoader: context.contextLoader,
235
+ documentLoader: context.documentLoader,
236
+ keyCache,
237
+ });
238
+ const jsonWithoutSig = detachSignature(json);
239
+ let activity = null;
240
+ if (ldSigVerified) {
241
+ logger.debug("Linked Data Signatures are verified.", { handle, json });
242
+ activity = await Activity.fromJsonLd(jsonWithoutSig, context);
239
243
  }
240
- catch (error) {
241
- logger.error("Failed to parse activity:\n{error}", { handle, json, error });
244
+ else {
245
+ logger.debug("Linked Data Signatures are not verified.", { handle, json });
242
246
  try {
243
- await inboxErrorHandler?.(context, error);
247
+ activity = await verifyObject(Activity, jsonWithoutSig, {
248
+ contextLoader: context.contextLoader,
249
+ documentLoader: context.documentLoader,
250
+ keyCache,
251
+ });
244
252
  }
245
253
  catch (error) {
246
- logger.error("An unexpected error occurred in inbox error handler:\n{error}", { error, activity: json });
254
+ logger.error("Failed to parse activity:\n{error}", {
255
+ handle,
256
+ json,
257
+ error,
258
+ });
259
+ try {
260
+ await inboxErrorHandler?.(context, error);
261
+ }
262
+ catch (error) {
263
+ logger.error("An unexpected error occurred in inbox error handler:\n{error}", { error, activity: json });
264
+ }
265
+ return new Response("Invalid activity.", {
266
+ status: 400,
267
+ headers: { "Content-Type": "text/plain; charset=utf-8" },
268
+ });
269
+ }
270
+ if (activity == null) {
271
+ logger.debug("Object Integrity Proofs are not verified.", { handle, json });
272
+ }
273
+ else {
274
+ logger.debug("Object Integrity Proofs are verified.", { handle, json });
247
275
  }
248
- return new Response("Invalid activity.", {
249
- status: 400,
250
- headers: { "Content-Type": "text/plain; charset=utf-8" },
251
- });
252
276
  }
253
277
  let httpSigKey = null;
254
278
  if (activity == null) {
@@ -260,16 +284,19 @@ export async function handleInbox(request, { handle, context, kv, kvPrefixes, qu
260
284
  keyCache,
261
285
  });
262
286
  if (key == null) {
263
- logger.error("Failed to verify the request signature.", { handle });
287
+ logger.error("Failed to verify the request's HTTP Signatures.", { handle });
264
288
  const response = new Response("Failed to verify the request signature.", {
265
289
  status: 401,
266
290
  headers: { "Content-Type": "text/plain; charset=utf-8" },
267
291
  });
268
292
  return response;
269
293
  }
294
+ else {
295
+ logger.debug("HTTP Signatures are verified.", { handle });
296
+ }
270
297
  httpSigKey = key;
271
298
  }
272
- activity = await Activity.fromJsonLd(json, context);
299
+ activity = await Activity.fromJsonLd(jsonWithoutSig, context);
273
300
  }
274
301
  const cacheKey = activity.id == null
275
302
  ? null
@@ -1,6 +1,7 @@
1
1
  import { getLogger } from "@logtape/logtape";
2
2
  import { signRequest } from "../sig/http.js";
3
3
  import { validateCryptoKey } from "../sig/key.js";
4
+ import { signJsonLd } from "../sig/ld.js";
4
5
  import { signObject } from "../sig/proof.js";
5
6
  /**
6
7
  * Extracts the inbox URLs from recipients.
@@ -45,8 +46,13 @@ export async function sendActivity({ activity, keys, inbox, contextLoader, docum
45
46
  }
46
47
  const activityId = activity.id.href;
47
48
  let proofCreated = false;
49
+ let rsaKey = null;
48
50
  for (const { keyId, privateKey } of keys) {
49
51
  validateCryptoKey(privateKey, "private");
52
+ if (rsaKey == null && privateKey.algorithm.name === "RSASSA-PKCS1-v1_5") {
53
+ rsaKey = { keyId, privateKey };
54
+ continue;
55
+ }
50
56
  if (privateKey.algorithm.name === "Ed25519") {
51
57
  activity = await signObject(activity, privateKey, keyId, {
52
58
  documentLoader,
@@ -55,6 +61,27 @@ export async function sendActivity({ activity, keys, inbox, contextLoader, docum
55
61
  proofCreated = true;
56
62
  }
57
63
  }
64
+ let jsonLd = await activity.toJsonLd({
65
+ format: "compact",
66
+ contextLoader,
67
+ });
68
+ if (rsaKey == null) {
69
+ logger.warn("No supported key found to create a Linked Data signature for " +
70
+ "the activity {activityId}. The activity will be sent without " +
71
+ "a Linked Data signature. In order to create a Linked Data " +
72
+ "signature, at least one RSASSA-PKCS1-v1_5 key must be provided.", {
73
+ activityId,
74
+ keys: keys.map((pair) => ({
75
+ keyId: pair.keyId.href,
76
+ privateKey: pair.privateKey,
77
+ })),
78
+ });
79
+ }
80
+ else {
81
+ jsonLd = await signJsonLd(jsonLd, rsaKey.privateKey, rsaKey.keyId, {
82
+ contextLoader,
83
+ });
84
+ }
58
85
  if (!proofCreated) {
59
86
  logger.warn("No supported key found to create a proof for the activity {activityId}. " +
60
87
  "The activity will be sent without a proof. " +
@@ -66,10 +93,6 @@ export async function sendActivity({ activity, keys, inbox, contextLoader, docum
66
93
  })),
67
94
  });
68
95
  }
69
- const jsonLd = await activity.toJsonLd({
70
- format: "compact",
71
- contextLoader,
72
- });
73
96
  headers = new Headers(headers);
74
97
  headers.set("Content-Type", "application/activity+json");
75
98
  let request = new Request(inbox, {
@@ -77,15 +100,7 @@ export async function sendActivity({ activity, keys, inbox, contextLoader, docum
77
100
  headers,
78
101
  body: JSON.stringify(jsonLd),
79
102
  });
80
- let requestSigned = false;
81
- for (const { privateKey, keyId } of keys) {
82
- if (privateKey.algorithm.name === "RSASSA-PKCS1-v1_5") {
83
- request = await signRequest(request, privateKey, keyId);
84
- requestSigned = true;
85
- break;
86
- }
87
- }
88
- if (!requestSigned) {
103
+ if (rsaKey == null) {
89
104
  logger.warn("No supported key found to sign the request to {inbox}. " +
90
105
  "The request will be sent without a signature. " +
91
106
  "In order to sign the request, at least one RSASSA-PKCS1-v1_5 key " +
@@ -97,6 +112,9 @@ export async function sendActivity({ activity, keys, inbox, contextLoader, docum
97
112
  })),
98
113
  });
99
114
  }
115
+ else {
116
+ request = await signRequest(request, rsaKey.privateKey, rsaKey.keyId);
117
+ }
100
118
  const response = await fetch(request);
101
119
  if (!response.ok) {
102
120
  let error;
@@ -625,5 +625,157 @@ const preloadedContexts = {
625
625
  },
626
626
  },
627
627
  },
628
+ "https://w3id.org/identity/v1": {
629
+ "@context": {
630
+ "id": "@id",
631
+ "type": "@type",
632
+ "cred": "https://w3id.org/credentials#",
633
+ "dc": "http://purl.org/dc/terms/",
634
+ "identity": "https://w3id.org/identity#",
635
+ "perm": "https://w3id.org/permissions#",
636
+ "ps": "https://w3id.org/payswarm#",
637
+ "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
638
+ "rdfs": "http://www.w3.org/2000/01/rdf-schema#",
639
+ "sec": "https://w3id.org/security#",
640
+ "schema": "http://schema.org/",
641
+ "xsd": "http://www.w3.org/2001/XMLSchema#",
642
+ "Group": "https://www.w3.org/ns/activitystreams#Group",
643
+ "claim": {
644
+ "@id": "cred:claim",
645
+ "@type": "@id",
646
+ },
647
+ "credential": {
648
+ "@id": "cred:credential",
649
+ "@type": "@id",
650
+ },
651
+ "issued": {
652
+ "@id": "cred:issued",
653
+ "@type": "xsd:dateTime",
654
+ },
655
+ "issuer": {
656
+ "@id": "cred:issuer",
657
+ "@type": "@id",
658
+ },
659
+ "recipient": {
660
+ "@id": "cred:recipient",
661
+ "@type": "@id",
662
+ },
663
+ "Credential": "cred:Credential",
664
+ "CryptographicKeyCredential": "cred:CryptographicKeyCredential",
665
+ "about": {
666
+ "@id": "schema:about",
667
+ "@type": "@id",
668
+ },
669
+ "address": {
670
+ "@id": "schema:address",
671
+ "@type": "@id",
672
+ },
673
+ "addressCountry": "schema:addressCountry",
674
+ "addressLocality": "schema:addressLocality",
675
+ "addressRegion": "schema:addressRegion",
676
+ "comment": "rdfs:comment",
677
+ "created": {
678
+ "@id": "dc:created",
679
+ "@type": "xsd:dateTime",
680
+ },
681
+ "creator": {
682
+ "@id": "dc:creator",
683
+ "@type": "@id",
684
+ },
685
+ "description": "schema:description",
686
+ "email": "schema:email",
687
+ "familyName": "schema:familyName",
688
+ "givenName": "schema:givenName",
689
+ "image": {
690
+ "@id": "schema:image",
691
+ "@type": "@id",
692
+ },
693
+ "label": "rdfs:label",
694
+ "name": "schema:name",
695
+ "postalCode": "schema:postalCode",
696
+ "streetAddress": "schema:streetAddress",
697
+ "title": "dc:title",
698
+ "url": {
699
+ "@id": "schema:url",
700
+ "@type": "@id",
701
+ },
702
+ "Person": "schema:Person",
703
+ "PostalAddress": "schema:PostalAddress",
704
+ "Organization": "schema:Organization",
705
+ "identityService": {
706
+ "@id": "identity:identityService",
707
+ "@type": "@id",
708
+ },
709
+ "idp": {
710
+ "@id": "identity:idp",
711
+ "@type": "@id",
712
+ },
713
+ "Identity": "identity:Identity",
714
+ "paymentProcessor": "ps:processor",
715
+ "preferences": {
716
+ "@id": "ps:preferences",
717
+ "@type": "@vocab",
718
+ },
719
+ "cipherAlgorithm": "sec:cipherAlgorithm",
720
+ "cipherData": "sec:cipherData",
721
+ "cipherKey": "sec:cipherKey",
722
+ "digestAlgorithm": "sec:digestAlgorithm",
723
+ "digestValue": "sec:digestValue",
724
+ "domain": "sec:domain",
725
+ "expires": {
726
+ "@id": "sec:expiration",
727
+ "@type": "xsd:dateTime",
728
+ },
729
+ "initializationVector": "sec:initializationVector",
730
+ "member": {
731
+ "@id": "schema:member",
732
+ "@type": "@id",
733
+ },
734
+ "memberOf": {
735
+ "@id": "schema:memberOf",
736
+ "@type": "@id",
737
+ },
738
+ "nonce": "sec:nonce",
739
+ "normalizationAlgorithm": "sec:normalizationAlgorithm",
740
+ "owner": {
741
+ "@id": "sec:owner",
742
+ "@type": "@id",
743
+ },
744
+ "password": "sec:password",
745
+ "privateKey": {
746
+ "@id": "sec:privateKey",
747
+ "@type": "@id",
748
+ },
749
+ "privateKeyPem": "sec:privateKeyPem",
750
+ "publicKey": {
751
+ "@id": "sec:publicKey",
752
+ "@type": "@id",
753
+ },
754
+ "publicKeyPem": "sec:publicKeyPem",
755
+ "publicKeyService": {
756
+ "@id": "sec:publicKeyService",
757
+ "@type": "@id",
758
+ },
759
+ "revoked": {
760
+ "@id": "sec:revoked",
761
+ "@type": "xsd:dateTime",
762
+ },
763
+ "signature": "sec:signature",
764
+ "signatureAlgorithm": "sec:signatureAlgorithm",
765
+ "signatureValue": "sec:signatureValue",
766
+ "CryptographicKey": "sec:Key",
767
+ "EncryptedMessage": "sec:EncryptedMessage",
768
+ "GraphSignature2012": "sec:GraphSignature2012",
769
+ "LinkedDataSignature2015": "sec:LinkedDataSignature2015",
770
+ "accessControl": {
771
+ "@id": "perm:accessControl",
772
+ "@type": "@id",
773
+ },
774
+ "writePermission": {
775
+ "@id": "perm:writePermission",
776
+ "@type": "@id",
777
+ },
778
+ },
779
+ },
628
780
  };
629
781
  export default preloadedContexts;
package/esm/sig/http.js CHANGED
@@ -96,7 +96,17 @@ export async function verifyRequest(request, { documentLoader, contextLoader, ti
96
96
  algo = algo.trim().toLowerCase();
97
97
  if (!(algo in supportedHashAlgorithms))
98
98
  continue;
99
- const digest = decodeBase64(digestBase64);
99
+ let digest;
100
+ try {
101
+ digest = decodeBase64(digestBase64);
102
+ }
103
+ catch (error) {
104
+ logger.debug("Failed to verify; invalid base64 encoding: {digest}.", {
105
+ digest: digestBase64,
106
+ error,
107
+ });
108
+ return null;
109
+ }
100
110
  const expectedDigest = await dntShim.crypto.subtle.digest(supportedHashAlgorithms[algo], body);
101
111
  if (!equals(digest, new Uint8Array(expectedDigest))) {
102
112
  logger.debug("Failed to verify; digest mismatch ({algorithm}): " +
package/esm/sig/ld.js ADDED
@@ -0,0 +1,203 @@
1
+ import * as dntShim from "../_dnt.shims.js";
2
+ import { getLogger } from "@logtape/logtape";
3
+ import { decodeBase64, encodeBase64 } from "../deps/jsr.io/@std/encoding/0.224.3/base64.js";
4
+ import { encodeHex } from "../deps/jsr.io/@std/encoding/0.224.3/hex.js";
5
+ // @ts-ignore TS7016
6
+ import jsonld from "jsonld";
7
+ import { fetchDocumentLoader, } from "../runtime/docloader.js";
8
+ import { Activity, CryptographicKey, Object } from "../vocab/vocab.js";
9
+ import { fetchKey, validateCryptoKey } from "./key.js";
10
+ const logger = getLogger(["fedify", "sig", "ld"]);
11
+ /**
12
+ * Attaches a LD signature to the given JSON-LD document.
13
+ * @param jsonLd The JSON-LD document to attach the signature to. It is not
14
+ * modified.
15
+ * @param signature The signature to attach.
16
+ * @returns The JSON-LD document with the attached signature.
17
+ * @throws {TypeError} If the input document is not a valid JSON-LD document.
18
+ * @since 1.0.0
19
+ */
20
+ export function attachSignature(jsonLd, signature) {
21
+ if (typeof jsonLd !== "object" || jsonLd == null) {
22
+ throw new TypeError("Failed to attach signature; invalid JSON-LD document.");
23
+ }
24
+ return { ...jsonLd, signature };
25
+ }
26
+ /**
27
+ * Creates a LD signature for the given JSON-LD document.
28
+ * @param jsonLd The JSON-LD document to sign.
29
+ * @param privateKey The private key to sign the document.
30
+ * @param keyId The ID of the public key that corresponds to the private key.
31
+ * @param options Additional options for creating the signature.
32
+ * See also {@link CreateSignatureOptions}.
33
+ * @return The created signature.
34
+ * @throws {TypeError} If the private key is invalid or unsupported.
35
+ * @since 1.0.0
36
+ */
37
+ export async function createSignature(jsonLd, privateKey, keyId, { contextLoader, created } = {}) {
38
+ validateCryptoKey(privateKey, "private");
39
+ if (privateKey.algorithm.name !== "RSASSA-PKCS1-v1_5") {
40
+ throw new TypeError("Unsupported algorithm: " + privateKey.algorithm.name);
41
+ }
42
+ const options = {
43
+ "@context": "https://w3id.org/identity/v1",
44
+ creator: keyId.href,
45
+ created: created?.toString() ?? new Date().toISOString(),
46
+ };
47
+ const optionsHash = await hashJsonLd(options, contextLoader);
48
+ const docHash = await hashJsonLd(jsonLd, contextLoader);
49
+ const message = optionsHash + docHash;
50
+ const encoder = new TextEncoder();
51
+ const messageBytes = encoder.encode(message);
52
+ const signature = await dntShim.crypto.subtle.sign("RSASSA-PKCS1-v1_5", privateKey, messageBytes);
53
+ return {
54
+ ...options,
55
+ type: "RsaSignature2017",
56
+ signatureValue: encodeBase64(signature),
57
+ };
58
+ }
59
+ /**
60
+ * Signs the given JSON-LD document with the private key and returns the signed
61
+ * JSON-LD document.
62
+ * @param jsonLd The JSON-LD document to sign.
63
+ * @param privateKey The private key to sign the document.
64
+ * @param keyId The key ID to use in the signature. It will be used by the
65
+ * verifier to fetch the corresponding public key.
66
+ * @param options Additional options for signing the document.
67
+ * See also {@link SignJsonLdOptions}.
68
+ * @returns The signed JSON-LD document.
69
+ * @throws {TypeError} If the private key is invalid or unsupported.
70
+ * @since 1.0.0
71
+ */
72
+ export async function signJsonLd(jsonLd, privateKey, keyId, options) {
73
+ const signature = await createSignature(jsonLd, privateKey, keyId, options);
74
+ return attachSignature(jsonLd, signature);
75
+ }
76
+ function hasSignature(jsonLd) {
77
+ if (typeof jsonLd !== "object" || jsonLd == null)
78
+ return false;
79
+ if ("signature" in jsonLd) {
80
+ const signature = jsonLd.signature;
81
+ if (typeof signature !== "object" || signature == null)
82
+ return false;
83
+ return "type" in signature && signature.type === "RsaSignature2017" &&
84
+ "creator" in signature && typeof signature.creator === "string" &&
85
+ "created" in signature && typeof signature.created === "string" &&
86
+ "signatureValue" in signature &&
87
+ typeof signature.signatureValue === "string";
88
+ }
89
+ return false;
90
+ }
91
+ /**
92
+ * Detaches Linked Data Signatures from the given JSON-LD document.
93
+ * @param jsonLd The JSON-LD document to modify.
94
+ * @returns The modified JSON-LD document. If the input document does not
95
+ * contain a signature, the original document is returned.
96
+ * @since 1.0.0
97
+ */
98
+ export function detachSignature(jsonLd) {
99
+ if (typeof jsonLd !== "object" || jsonLd == null)
100
+ return jsonLd;
101
+ const doc = { ...jsonLd };
102
+ delete doc.signature;
103
+ return doc;
104
+ }
105
+ /**
106
+ * Verifies Linked Data Signatures of the given JSON-LD document.
107
+ * @param jsonLd The JSON-LD document to verify.
108
+ * @param options Options for verifying the signature.
109
+ * @returns The public key that signed the document or `null` if the signature
110
+ * is invalid or the key is not found.
111
+ * @since 1.0.0
112
+ */
113
+ export async function verifySignature(jsonLd, options = {}) {
114
+ if (!hasSignature(jsonLd))
115
+ return null;
116
+ const sig = jsonLd.signature;
117
+ let signature;
118
+ try {
119
+ signature = decodeBase64(sig.signatureValue);
120
+ }
121
+ catch (error) {
122
+ logger.debug("Failed to verify; invalid base64 signatureValue: {signatureValue}", { ...sig, error });
123
+ return null;
124
+ }
125
+ const keyResult = await fetchKey(new URL(sig.creator), CryptographicKey, options);
126
+ if (keyResult == null)
127
+ return null;
128
+ const { key, cached } = keyResult;
129
+ const sigOpts = {
130
+ ...sig,
131
+ "@context": "https://w3id.org/identity/v1",
132
+ };
133
+ delete sigOpts.type;
134
+ delete sigOpts.id;
135
+ delete sigOpts.signatureValue;
136
+ const sigOptsHash = await hashJsonLd(sigOpts, options.contextLoader);
137
+ const document = { ...jsonLd };
138
+ delete document.signature;
139
+ const docHash = await hashJsonLd(document, options.contextLoader);
140
+ const encoder = new TextEncoder();
141
+ const message = sigOptsHash + docHash;
142
+ const messageBytes = encoder.encode(message);
143
+ const verified = await dntShim.crypto.subtle.verify("RSASSA-PKCS1-v1_5", key.publicKey, signature, messageBytes);
144
+ if (verified)
145
+ return key;
146
+ if (cached) {
147
+ logger.debug("Failed to verify with the cached key {keyId}; " +
148
+ "signature {signatureValue} is invalid. " +
149
+ "Retrying with the freshly fetched key...", { keyId: sig.creator, ...sig });
150
+ const keyResult = await fetchKey(new URL(sig.creator), CryptographicKey, { ...options, keyCache: undefined });
151
+ if (keyResult == null)
152
+ return null;
153
+ const { key } = keyResult;
154
+ const verified = await dntShim.crypto.subtle.verify("RSASSA-PKCS1-v1_5", key.publicKey, signature, messageBytes);
155
+ return verified ? key : null;
156
+ }
157
+ logger.debug("Failed to verify with the fetched key {keyId}; " +
158
+ "signature {signatureValue} is invalid. " +
159
+ "Check if the key is correct or if the signed message is correct. " +
160
+ "The message to sign is:\n{message}", { keyId: sig.creator, ...sig, message });
161
+ return null;
162
+ }
163
+ /**
164
+ * Verify the authenticity of the given JSON-LD document using Linked Data
165
+ * Signatures. If the document is signed, this function verifies the signature
166
+ * and checks if the document is attributed to the owner of the public key.
167
+ * If the document is not signed, this function returns `false`.
168
+ * @param jsonLd The JSON-LD document to verify.
169
+ * @param options Options for verifying the document.
170
+ * @returns `true` if the document is authentic; `false` otherwise.
171
+ */
172
+ export async function verifyJsonLd(jsonLd, options = {}) {
173
+ const object = await Object.fromJsonLd(jsonLd, options);
174
+ const attributions = new Set(object.attributionIds.map((uri) => uri.href));
175
+ if (object instanceof Activity) {
176
+ for (const uri of object.actorIds)
177
+ attributions.add(uri.href);
178
+ }
179
+ const key = await verifySignature(jsonLd, options);
180
+ if (key == null)
181
+ return false;
182
+ if (key.ownerId == null) {
183
+ logger.debug("Key {keyId} has no owner.", { keyId: key.id?.href });
184
+ return false;
185
+ }
186
+ attributions.delete(key.ownerId.href);
187
+ if (attributions.size > 0) {
188
+ logger.debug("Some attributions are not authenticated by the Linked Data Signatures" +
189
+ ": {attributions}.", { attributions: [...attributions] });
190
+ return false;
191
+ }
192
+ return true;
193
+ }
194
+ async function hashJsonLd(jsonLd, contextLoader) {
195
+ const canon = await jsonld.canonize(jsonLd, {
196
+ format: "application/n-quads",
197
+ documentLoader: contextLoader ?? fetchDocumentLoader,
198
+ });
199
+ const encoder = new TextEncoder();
200
+ const hash = await dntShim.crypto.subtle.digest("SHA-256", encoder.encode(canon));
201
+ return encodeHex(hash);
202
+ }
203
+ // cSpell: ignore URGNA2012
package/esm/sig/mod.js CHANGED
@@ -5,5 +5,6 @@
5
5
  */
6
6
  export { signRequest, verifyRequest, } from "./http.js";
7
7
  export { exportJwk, generateCryptoKeyPair, importJwk, } from "./key.js";
8
+ export * from "./ld.js";
8
9
  export { doesActorOwnKey, getKeyOwner, } from "./owner.js";
9
10
  export * from "./proof.js";