@fedify/fedify 0.15.0 → 1.0.0-dev.388
Sign up to get free protection for your applications and to get access to all the features.
- 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