@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.
- package/CHANGES.md +33 -0
- package/FEDERATION.md +2 -0
- package/esm/federation/handler.js +44 -17
- package/esm/federation/send.js +31 -13
- package/esm/runtime/contexts.js +152 -0
- package/esm/sig/http.js +11 -1
- package/esm/sig/ld.js +203 -0
- package/esm/sig/mod.js +1 -0
- package/esm/testing/fixtures/activitypub.academy/users/brauca_darradiul +83 -0
- package/esm/testing/fixtures/w3id.org/identity/v1 +152 -0
- package/esm/vocab/accept.yaml +1 -0
- package/esm/vocab/activity.yaml +1 -0
- package/esm/vocab/add.yaml +1 -0
- package/esm/vocab/announce.yaml +1 -0
- package/esm/vocab/arrive.yaml +1 -0
- package/esm/vocab/block.yaml +1 -0
- package/esm/vocab/create.yaml +1 -0
- package/esm/vocab/delete.yaml +1 -0
- package/esm/vocab/dislike.yaml +1 -0
- package/esm/vocab/flag.yaml +1 -0
- package/esm/vocab/follow.yaml +1 -0
- package/esm/vocab/ignore.yaml +1 -0
- package/esm/vocab/intransitiveactivity.yaml +1 -0
- package/esm/vocab/invite.yaml +1 -0
- package/esm/vocab/join.yaml +1 -0
- package/esm/vocab/leave.yaml +1 -0
- package/esm/vocab/like.yaml +1 -0
- package/esm/vocab/listen.yaml +1 -0
- package/esm/vocab/move.yaml +1 -0
- package/esm/vocab/offer.yaml +1 -0
- package/esm/vocab/question.yaml +1 -0
- package/esm/vocab/read.yaml +1 -0
- package/esm/vocab/reject.yaml +1 -0
- package/esm/vocab/remove.yaml +1 -0
- package/esm/vocab/tentativeaccept.yaml +1 -0
- package/esm/vocab/tentativereject.yaml +1 -0
- package/esm/vocab/travel.yaml +1 -0
- package/esm/vocab/undo.yaml +1 -0
- package/esm/vocab/update.yaml +1 -0
- package/esm/vocab/view.yaml +1 -0
- package/esm/vocab/vocab.js +59 -0
- package/package.json +1 -1
- package/types/federation/handler.d.ts.map +1 -1
- package/types/federation/send.d.ts.map +1 -1
- package/types/runtime/contexts.d.ts.map +1 -1
- package/types/sig/http.d.ts.map +1 -1
- package/types/sig/ld.d.ts +129 -0
- package/types/sig/ld.d.ts.map +1 -0
- package/types/sig/mod.d.ts +1 -0
- package/types/sig/mod.d.ts.map +1 -1
- 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
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
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
|
-
|
241
|
-
logger.
|
244
|
+
else {
|
245
|
+
logger.debug("Linked Data Signatures are not verified.", { handle, json });
|
242
246
|
try {
|
243
|
-
await
|
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("
|
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
|
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(
|
299
|
+
activity = await Activity.fromJsonLd(jsonWithoutSig, context);
|
273
300
|
}
|
274
301
|
const cacheKey = activity.id == null
|
275
302
|
? null
|
package/esm/federation/send.js
CHANGED
@@ -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
|
-
|
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;
|
package/esm/runtime/contexts.js
CHANGED
@@ -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
|
-
|
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