@fedify/fedify 1.0.0-dev.386 → 1.0.0-dev.391

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. package/CHANGES.md +27 -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
@@ -8,6 +8,33 @@ Version 1.0.0
8
8
 
9
9
  To be released.
10
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
+
11
38
 
12
39
  Version 0.15.0
13
40
  --------------
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";