cmem-plugin-graphsigning 0.5.0__py3-none-any.whl
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.
- cmem_plugin_graphsigning/__init__.py +1 -0
- cmem_plugin_graphsigning/canonicalization.py +173 -0
- cmem_plugin_graphsigning/create_graph_signature.py +835 -0
- cmem_plugin_graphsigning/create_graph_signature.svg +1 -0
- cmem_plugin_graphsigning/graph_proof_utils.py +341 -0
- cmem_plugin_graphsigning/graph_store.py +95 -0
- cmem_plugin_graphsigning/remove_graph_signature.py +147 -0
- cmem_plugin_graphsigning/remove_graph_signature.svg +1 -0
- cmem_plugin_graphsigning/signing.py +259 -0
- cmem_plugin_graphsigning/verify_graph_signature.py +875 -0
- cmem_plugin_graphsigning/verify_graph_signature.svg +1 -0
- cmem_plugin_graphsigning/vocabulary.py +48 -0
- cmem_plugin_graphsigning-0.5.0.dist-info/METADATA +50 -0
- cmem_plugin_graphsigning-0.5.0.dist-info/RECORD +16 -0
- cmem_plugin_graphsigning-0.5.0.dist-info/WHEEL +4 -0
- cmem_plugin_graphsigning-0.5.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,835 @@
|
|
|
1
|
+
"""Create Graph Signature Task"""
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import re
|
|
5
|
+
import tempfile
|
|
6
|
+
from collections import OrderedDict
|
|
7
|
+
from collections.abc import Callable, Sequence
|
|
8
|
+
from contextlib import suppress
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from datetime import UTC, datetime, timedelta
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
import rdflib
|
|
14
|
+
from cmem_client.client import Client
|
|
15
|
+
from cmem_client.repositories.protocols.import_item import ImportConflictPolicy
|
|
16
|
+
from cmem_plugin_base.dataintegration.client import get_client
|
|
17
|
+
from cmem_plugin_base.dataintegration.context import ExecutionContext, ExecutionReport
|
|
18
|
+
from cmem_plugin_base.dataintegration.description import (
|
|
19
|
+
Icon,
|
|
20
|
+
Plugin,
|
|
21
|
+
PluginAction,
|
|
22
|
+
PluginParameter,
|
|
23
|
+
)
|
|
24
|
+
from cmem_plugin_base.dataintegration.entity import (
|
|
25
|
+
Entities,
|
|
26
|
+
Entity,
|
|
27
|
+
EntitySchema,
|
|
28
|
+
)
|
|
29
|
+
from cmem_plugin_base.dataintegration.parameter.choice import ChoiceParameterType
|
|
30
|
+
from cmem_plugin_base.dataintegration.parameter.graph import GraphParameterType
|
|
31
|
+
from cmem_plugin_base.dataintegration.parameter.password import Password, PasswordParameterType
|
|
32
|
+
from cmem_plugin_base.dataintegration.plugins import WorkflowPlugin
|
|
33
|
+
from cmem_plugin_base.dataintegration.ports import (
|
|
34
|
+
FixedNumberOfInputs,
|
|
35
|
+
FixedSchemaPort,
|
|
36
|
+
)
|
|
37
|
+
from cmem_plugin_base.dataintegration.typed_entities.file import FileEntitySchema, LocalFile
|
|
38
|
+
from cryptography.hazmat.primitives import serialization
|
|
39
|
+
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
|
40
|
+
from cryptography.hazmat.primitives.asymmetric.rsa import generate_private_key
|
|
41
|
+
from cryptography.hazmat.primitives.asymmetric.types import PrivateKeyTypes, PublicKeyTypes
|
|
42
|
+
from rdflib import RDF, URIRef
|
|
43
|
+
|
|
44
|
+
from cmem_plugin_graphsigning.graph_proof_utils import (
|
|
45
|
+
ProofMetadata,
|
|
46
|
+
build_digest_record,
|
|
47
|
+
build_proof_rdf,
|
|
48
|
+
canonical_digest,
|
|
49
|
+
multibase_decode,
|
|
50
|
+
new_proof_uri,
|
|
51
|
+
new_rdf_graph,
|
|
52
|
+
proof_graph_iri,
|
|
53
|
+
proof_payload,
|
|
54
|
+
proofs_by_other_keys,
|
|
55
|
+
signature_options,
|
|
56
|
+
)
|
|
57
|
+
from cmem_plugin_graphsigning.graph_store import (
|
|
58
|
+
download_and_parse,
|
|
59
|
+
download_and_parse_proof_graph,
|
|
60
|
+
graph_iri_input_schema,
|
|
61
|
+
hash_parsed_graph,
|
|
62
|
+
)
|
|
63
|
+
from cmem_plugin_graphsigning.signing import (
|
|
64
|
+
MINIMUM_RSA_KEY_SIZE,
|
|
65
|
+
check_signing_key_strength,
|
|
66
|
+
cryptosuite_for,
|
|
67
|
+
did_key_controller,
|
|
68
|
+
is_multikey,
|
|
69
|
+
public_key_from_did_key,
|
|
70
|
+
public_key_multibase,
|
|
71
|
+
sign_digest,
|
|
72
|
+
verification_method_id,
|
|
73
|
+
verify_digest,
|
|
74
|
+
)
|
|
75
|
+
from cmem_plugin_graphsigning.vocabulary import SEC
|
|
76
|
+
|
|
77
|
+
OUTPUT_ONE_DOCUMENT = "one-document"
|
|
78
|
+
OUTPUT_DOCUMENT_PER_GRAPH = "document-per-graph"
|
|
79
|
+
OUTPUT_INTO_GRAPH = "into-graph"
|
|
80
|
+
|
|
81
|
+
OUTPUT_CHOICES = OrderedDict(
|
|
82
|
+
[
|
|
83
|
+
(
|
|
84
|
+
OUTPUT_ONE_DOCUMENT,
|
|
85
|
+
("One JSON-LD document - every graph's signature in a single file"),
|
|
86
|
+
),
|
|
87
|
+
(
|
|
88
|
+
OUTPUT_DOCUMENT_PER_GRAPH,
|
|
89
|
+
(
|
|
90
|
+
"One JSON-LD document per graph - each signature its own file, to keep "
|
|
91
|
+
"or send on separately"
|
|
92
|
+
),
|
|
93
|
+
),
|
|
94
|
+
(
|
|
95
|
+
OUTPUT_INTO_GRAPH,
|
|
96
|
+
(
|
|
97
|
+
"Into a graph of its own - a companion proof graph beside the source graph, "
|
|
98
|
+
"which is left untouched; the task hands nothing on"
|
|
99
|
+
),
|
|
100
|
+
),
|
|
101
|
+
]
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
_PROOF_VALUE_PLACEHOLDER = "cGxhY2Vob2xkZXI="
|
|
105
|
+
"""Stands in for the signature while the triples it will cover are assembled."""
|
|
106
|
+
|
|
107
|
+
GENERATED_RSA_KEY_SIZE = 3072
|
|
108
|
+
"""Modulus, in bits, of the RSA keys the *Create RSA Key Pair* action issues.
|
|
109
|
+
|
|
110
|
+
The shortest widely implemented length at or above ``signing.MINIMUM_RSA_KEY_SIZE``,
|
|
111
|
+
so a key this action hands out is one the signing path accepts. The length is not
|
|
112
|
+
configurable: an action that could issue a key the task then refuses would be a worse
|
|
113
|
+
offer than one that issues a single good length.
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@dataclass(frozen=True)
|
|
118
|
+
class ProofWrite:
|
|
119
|
+
"""Everything one proof write needs about one graph."""
|
|
120
|
+
|
|
121
|
+
graph_iri: str
|
|
122
|
+
rdf_hash: str
|
|
123
|
+
private_key: PrivateKeyTypes
|
|
124
|
+
created: str
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@Plugin(
|
|
128
|
+
label="Create Graph Signature",
|
|
129
|
+
plugin_id="cmem_plugin_graphsigning-CreateGraphSignature",
|
|
130
|
+
description=(
|
|
131
|
+
"Compute a canonical SHA-256 digest of Knowledge Graphs and optionally sign it "
|
|
132
|
+
"as a W3C Data Integrity proof."
|
|
133
|
+
),
|
|
134
|
+
documentation="""
|
|
135
|
+
Compute a canonical SHA-256 digest (RDFC-1.0) of one or more Knowledge Graphs and, given a
|
|
136
|
+
private key, sign each digest as a W3C Data Integrity proof. Without a key the task only
|
|
137
|
+
hashes.
|
|
138
|
+
|
|
139
|
+
## Ports
|
|
140
|
+
|
|
141
|
+
Graph IRIs arrive on the input port, as `graph_iri` values, and are processed in order. The
|
|
142
|
+
port is replaced by a parameter when a single graph is configured on the task itself. Keys
|
|
143
|
+
never arrive on a port.
|
|
144
|
+
|
|
145
|
+
What leaves depends on where the signature goes. Written to documents, it leaves the output
|
|
146
|
+
port as JSON-LD files, one for the whole run or one per graph, ready to be stored in a file
|
|
147
|
+
dataset beside the data. Written into a companion proof graph, it is already in the store, and
|
|
148
|
+
the task declares no output port at all. A run without a key hands on documents that carry the
|
|
149
|
+
digest alone, which still record what the content hashed to.
|
|
150
|
+
|
|
151
|
+
## Where this task fits
|
|
152
|
+
|
|
153
|
+
Every shape this task produces is one **Verify Graph Signature** reads, and the two default to
|
|
154
|
+
the same one, so they wire together untouched. **Remove Graph Signature** takes the
|
|
155
|
+
signatures back off a graph, and its output feeds this task again to sign afresh.
|
|
156
|
+
|
|
157
|
+
## What a proof says
|
|
158
|
+
|
|
159
|
+
A proof names the key that made it, states its purpose as `sec:assertionMethod`, and records
|
|
160
|
+
the algorithm in `sec:cryptosuite`. Its signature covers all of that together with the digest,
|
|
161
|
+
so no statement in a proof can be edited afterwards without invalidating it.
|
|
162
|
+
|
|
163
|
+
An Ed25519 key names itself through its `did:key`, derived from the key material, so it is
|
|
164
|
+
named identically in every graph it signs and nothing has to be configured; that identifier
|
|
165
|
+
contains the key, so an Ed25519 proof always discloses its public key.
|
|
166
|
+
|
|
167
|
+
Which key type to use is a choice between interoperability and a recommendation. Ed25519 is
|
|
168
|
+
recommended here on interoperability grounds: its keys and signatures are small, it has no
|
|
169
|
+
padding or hash settings to get wrong, and its proofs follow the registered
|
|
170
|
+
`eddsa-rdfc-2022` cryptosuite, so another conformant implementation checks exactly the bytes
|
|
171
|
+
this task signed. It is, however, recommended by no part of BSI TR-02102-1. RSA-PSS is, at a
|
|
172
|
+
modulus of 3000 bits or more, which is the floor this task enforces when it is configured -
|
|
173
|
+
but RSA proofs use this plugin's own `rsa-rdfc1-sha256-2026` cryptosuite, which only
|
|
174
|
+
**Verify Graph Signature** checks. So choose RSA where an organizational key has to be used,
|
|
175
|
+
or where TR-02102-1 has to be followed, and Ed25519 where a proof has to be verifiable by
|
|
176
|
+
tooling other than this plugin.
|
|
177
|
+
|
|
178
|
+
## Caveats
|
|
179
|
+
|
|
180
|
+
- **The digest covers content, not the graph it came from.** The same triples hash the same
|
|
181
|
+
under any IRI, or loaded from a file. The link from a proof to its graph is not signed, so a
|
|
182
|
+
proof moved to another graph with identical content verifies there too.
|
|
183
|
+
- Writing into a companion proof graph replaces your own earlier signature and keeps those
|
|
184
|
+
made with other keys; taking those off is what **Remove Graph Signature** is for. A proof is
|
|
185
|
+
yours when it names your verification method or your key's `did:key`, or when your key
|
|
186
|
+
verifies it against the content as it is now. Re-signing unchanged content yields the same
|
|
187
|
+
digest and a new proof, since two signing acts are two proofs.
|
|
188
|
+
- A `sec:proof` resource in the companion proof graph that is not typed
|
|
189
|
+
`sec:DataIntegrityProof` is not recognized as a proof, and is dropped the next time any
|
|
190
|
+
signature is written into that graph.
|
|
191
|
+
- The companion proof graph is read, changed and replaced in one step per graph. Two runs
|
|
192
|
+
signing the same graph with different keys at the same time can lose one of the two
|
|
193
|
+
signatures.
|
|
194
|
+
- The source graph is never written to. Proof-shaped triples already sitting in it are not
|
|
195
|
+
recognized as a signature, and are hashed and signed as ordinary data.
|
|
196
|
+
- A proof naming its key through `did:key`, or embedding it, verifies on its own but does not
|
|
197
|
+
say who signed. Anyone able to write the companion proof graph can re-sign altered content
|
|
198
|
+
with their own key; a verifier has to check against a key it trusts.
|
|
199
|
+
- With one document for the whole run, a cancelled run writes no document at all rather than
|
|
200
|
+
one covering only the graphs done so far, which would read as a complete, passing set. With
|
|
201
|
+
one document per graph, the documents for the graphs done so far are handed on.
|
|
202
|
+
- A graph that does not exist fails the run. Canonicalization is capped at five seconds per
|
|
203
|
+
graph; a graph large or blank-node-heavy enough to exceed that fails the run with a timeout.
|
|
204
|
+
- A key that is neither Ed25519 nor RSA, an RSA key with a modulus under 3000 bits, a key the
|
|
205
|
+
passphrase does not open, and incomplete combinations - such as an RSA key with no
|
|
206
|
+
verification method - are refused when the task is configured, not when it runs.
|
|
207
|
+
- A signature document is this plugin's own format, not a W3C secured document: it holds the
|
|
208
|
+
proof apart from the data it covers. A standard verifier needs a secured document assembled
|
|
209
|
+
from the graph's data and this proof first.
|
|
210
|
+
""",
|
|
211
|
+
icon=Icon(file_name="create_graph_signature.svg", package=__package__),
|
|
212
|
+
actions=[
|
|
213
|
+
PluginAction(
|
|
214
|
+
name="create_key_pair",
|
|
215
|
+
label="Create Ed25519 Key Pair",
|
|
216
|
+
description=(
|
|
217
|
+
"Generate a new Ed25519 key pair and show it once, without storing it "
|
|
218
|
+
"anywhere. This is the recommended key type for interoperability, but it "
|
|
219
|
+
"is outside BSI TR-02102-1; where that recommendation has to be followed, "
|
|
220
|
+
"create an RSA key pair instead. Set Private Key Passphrase first to have "
|
|
221
|
+
"the generated private key encrypted with it."
|
|
222
|
+
),
|
|
223
|
+
),
|
|
224
|
+
PluginAction(
|
|
225
|
+
name="create_rsa_key_pair",
|
|
226
|
+
label="Create RSA Key Pair",
|
|
227
|
+
description=(
|
|
228
|
+
"Generate a new RSA key pair and show it once, without storing it anywhere. "
|
|
229
|
+
f"The key has a {GENERATED_RSA_KEY_SIZE} bit modulus, which is the shortest "
|
|
230
|
+
"this task signs with. Use this where an organizational RSA key is required, "
|
|
231
|
+
"or where BSI TR-02102-1 has to be followed. Set Private Key Passphrase "
|
|
232
|
+
"first to have the generated private key encrypted with it."
|
|
233
|
+
),
|
|
234
|
+
),
|
|
235
|
+
],
|
|
236
|
+
parameters=[
|
|
237
|
+
PluginParameter(
|
|
238
|
+
name="input_graph",
|
|
239
|
+
label="Input Graph",
|
|
240
|
+
description=(
|
|
241
|
+
"The Knowledge Graph to hash. Leave empty to take graph IRIs from the input "
|
|
242
|
+
"port instead; setting this parameter removes that port."
|
|
243
|
+
),
|
|
244
|
+
param_type=GraphParameterType(
|
|
245
|
+
show_di_graphs=False,
|
|
246
|
+
show_graphs_without_class=True,
|
|
247
|
+
show_system_graphs=True,
|
|
248
|
+
allow_only_autocompleted_values=False,
|
|
249
|
+
),
|
|
250
|
+
default_value="",
|
|
251
|
+
),
|
|
252
|
+
PluginParameter(
|
|
253
|
+
name="private_key",
|
|
254
|
+
label="Private Key",
|
|
255
|
+
description=(
|
|
256
|
+
"PEM-encoded Ed25519 or RSA private key that signs each digest. Leave "
|
|
257
|
+
"empty to hash without signing. The key type decides the algorithm, so "
|
|
258
|
+
"there is nothing else to choose. An RSA key needs a modulus of at least "
|
|
259
|
+
f"{MINIMUM_RSA_KEY_SIZE} bits."
|
|
260
|
+
),
|
|
261
|
+
param_type=PasswordParameterType(),
|
|
262
|
+
default_value="",
|
|
263
|
+
),
|
|
264
|
+
PluginParameter(
|
|
265
|
+
name="private_key_passphrase",
|
|
266
|
+
label="Private Key Passphrase",
|
|
267
|
+
description=(
|
|
268
|
+
"Passphrase of an encrypted private key. The key-pair actions encrypt the "
|
|
269
|
+
"private key they generate with it."
|
|
270
|
+
),
|
|
271
|
+
param_type=PasswordParameterType(),
|
|
272
|
+
default_value="",
|
|
273
|
+
),
|
|
274
|
+
PluginParameter(
|
|
275
|
+
name="output_shape",
|
|
276
|
+
label="Output",
|
|
277
|
+
description=(
|
|
278
|
+
"Where each signature goes. Writing into a graph of its own requires a "
|
|
279
|
+
"Private Key and removes the output port."
|
|
280
|
+
),
|
|
281
|
+
param_type=ChoiceParameterType(OUTPUT_CHOICES),
|
|
282
|
+
default_value=OUTPUT_ONE_DOCUMENT,
|
|
283
|
+
),
|
|
284
|
+
PluginParameter(
|
|
285
|
+
name="embed_public_key",
|
|
286
|
+
label="Embed Public Key",
|
|
287
|
+
description=(
|
|
288
|
+
"If enabled, embeds the signing key's public key in each proof as "
|
|
289
|
+
"sec:publicKeyMultibase, so the proof can be checked without a key supplied "
|
|
290
|
+
"separately. An Ed25519 proof needs no embedded key for that, since its "
|
|
291
|
+
"did:key already names the key. Requires a Private Key."
|
|
292
|
+
),
|
|
293
|
+
default_value=False,
|
|
294
|
+
advanced=True,
|
|
295
|
+
),
|
|
296
|
+
PluginParameter(
|
|
297
|
+
name="verification_method_iri",
|
|
298
|
+
label="Verification Method IRI",
|
|
299
|
+
description=(
|
|
300
|
+
"IRI naming the signing key, written as each proof's sec:verificationMethod. "
|
|
301
|
+
"Leave empty for an Ed25519 key, which is then named by its did:key; an RSA "
|
|
302
|
+
"key requires it."
|
|
303
|
+
),
|
|
304
|
+
default_value="",
|
|
305
|
+
advanced=True,
|
|
306
|
+
),
|
|
307
|
+
PluginParameter(
|
|
308
|
+
name="resolve_owl_imports",
|
|
309
|
+
label="Resolve owl:imports",
|
|
310
|
+
description=(
|
|
311
|
+
"If enabled, resolves owl:imports before hashing, so the digest and signature "
|
|
312
|
+
"cover every graph reachable through the source graph's owl:imports. Verify "
|
|
313
|
+
"Graph Signature has to use the same setting."
|
|
314
|
+
),
|
|
315
|
+
default_value=False,
|
|
316
|
+
advanced=True,
|
|
317
|
+
),
|
|
318
|
+
PluginParameter(
|
|
319
|
+
name="proof_validity_days",
|
|
320
|
+
label="Proof Validity (days)",
|
|
321
|
+
description=(
|
|
322
|
+
"Number of days after signing at which each proof expires, written as "
|
|
323
|
+
"sec:expiration. 0 writes proofs that never expire."
|
|
324
|
+
),
|
|
325
|
+
default_value=0,
|
|
326
|
+
advanced=True,
|
|
327
|
+
),
|
|
328
|
+
],
|
|
329
|
+
)
|
|
330
|
+
class CreateGraphSignature(WorkflowPlugin):
|
|
331
|
+
"""Compute a hash value from one or more Knowledge Graphs, with optional signing."""
|
|
332
|
+
|
|
333
|
+
# A plugin constructor takes one argument per PluginParameter, so its arity is
|
|
334
|
+
# fixed by the plugin's configuration surface, not by a style choice here.
|
|
335
|
+
def __init__( # noqa: PLR0913, PLR0917
|
|
336
|
+
self,
|
|
337
|
+
input_graph: str = "",
|
|
338
|
+
private_key: Password | str = "",
|
|
339
|
+
private_key_passphrase: Password | str = "",
|
|
340
|
+
output_shape: str = OUTPUT_ONE_DOCUMENT,
|
|
341
|
+
embed_public_key: bool = False,
|
|
342
|
+
verification_method_iri: str = "",
|
|
343
|
+
resolve_owl_imports: bool = False,
|
|
344
|
+
proof_validity_days: int = 0,
|
|
345
|
+
) -> None:
|
|
346
|
+
# A Password parameter is always truthy - even when the field was left blank it
|
|
347
|
+
# arrives as Password(""), so whether a key was given can only be told from the
|
|
348
|
+
# decrypted value. Only the answer is kept; the key itself stays encrypted until
|
|
349
|
+
# execute() needs it.
|
|
350
|
+
self.signing = bool(self._resolve_secret(private_key))
|
|
351
|
+
if self.signing:
|
|
352
|
+
# Refuse a key this plugin will not sign with here, rather than at execute
|
|
353
|
+
# time: cryptosuite_for raises for anything but Ed25519 and RSA, and
|
|
354
|
+
# check_signing_key_strength for an RSA key below the policy minimum.
|
|
355
|
+
key = self._load_private_key(private_key, private_key_passphrase)
|
|
356
|
+
cryptosuite_for(key)
|
|
357
|
+
check_signing_key_strength(key)
|
|
358
|
+
if embed_public_key and not self.signing:
|
|
359
|
+
raise ValueError("'Embed Public Key' requires a private key to be provided.")
|
|
360
|
+
if output_shape == OUTPUT_INTO_GRAPH and not self.signing:
|
|
361
|
+
raise ValueError(
|
|
362
|
+
"Writing into a graph of its own requires a private key to be provided."
|
|
363
|
+
)
|
|
364
|
+
if proof_validity_days < 0:
|
|
365
|
+
raise ValueError("'Proof Validity (days)' must not be negative.")
|
|
366
|
+
self.proof_validity_days = proof_validity_days
|
|
367
|
+
self.input_graph = input_graph
|
|
368
|
+
self.private_key = private_key
|
|
369
|
+
self.embed_public_key = embed_public_key
|
|
370
|
+
if self.signing and not verification_method_iri:
|
|
371
|
+
# Every proof has to name its key. An Ed25519 key identifies itself: its
|
|
372
|
+
# did:key is derived from the key material, so it needs no IRI of its own.
|
|
373
|
+
# Nothing else can do that.
|
|
374
|
+
public_key = self._load_private_key(private_key, private_key_passphrase).public_key()
|
|
375
|
+
if not is_multikey(public_key):
|
|
376
|
+
raise ValueError(
|
|
377
|
+
"Signing with this key type requires a 'Verification Method IRI' to "
|
|
378
|
+
"be provided. Every proof names the key that made it, and only an "
|
|
379
|
+
"Ed25519 key can identify itself, through its did:key."
|
|
380
|
+
)
|
|
381
|
+
self.private_key_passphrase = private_key_passphrase
|
|
382
|
+
self.resolve_owl_imports = resolve_owl_imports
|
|
383
|
+
if output_shape not in OUTPUT_CHOICES:
|
|
384
|
+
raise ValueError(f"'Output' must be one of {', '.join(OUTPUT_CHOICES)}.")
|
|
385
|
+
self.output_shape = output_shape
|
|
386
|
+
self.verification_method_iri = verification_method_iri
|
|
387
|
+
if input_graph:
|
|
388
|
+
self.input_ports = FixedNumberOfInputs([])
|
|
389
|
+
else:
|
|
390
|
+
self.input_ports = FixedNumberOfInputs(
|
|
391
|
+
[FixedSchemaPort(schema=graph_iri_input_schema())]
|
|
392
|
+
)
|
|
393
|
+
self._documents_dir: Path | None = None
|
|
394
|
+
schema = self._output_entity_schema()
|
|
395
|
+
self.output_port = None if schema is None else FixedSchemaPort(schema=schema)
|
|
396
|
+
|
|
397
|
+
def _emit_for_graph(
|
|
398
|
+
self,
|
|
399
|
+
graph_iri: str,
|
|
400
|
+
rdf_hash: str,
|
|
401
|
+
proof_rdf: rdflib.Graph | None,
|
|
402
|
+
collected: rdflib.Graph,
|
|
403
|
+
) -> Entity | None:
|
|
404
|
+
"""Return the document one graph contributes to the output, or accumulate it.
|
|
405
|
+
|
|
406
|
+
Only the one-document-per-graph mode returns anything. The one-document-per-run
|
|
407
|
+
mode collects the graphs and writes them once the last of them has been signed,
|
|
408
|
+
and writing into a graph of its own hands nothing on.
|
|
409
|
+
"""
|
|
410
|
+
if not self._writes_documents:
|
|
411
|
+
return None
|
|
412
|
+
signatures = proof_rdf if proof_rdf is not None else self._digest_only(graph_iri, rdf_hash)
|
|
413
|
+
if self.output_shape == OUTPUT_DOCUMENT_PER_GRAPH:
|
|
414
|
+
return FileEntitySchema().to_entity(self.signature_document(graph_iri, signatures))
|
|
415
|
+
for triple in signatures:
|
|
416
|
+
collected.add(triple)
|
|
417
|
+
return None
|
|
418
|
+
|
|
419
|
+
@property
|
|
420
|
+
def _writes_documents(self) -> bool:
|
|
421
|
+
"""Whether this run hands signature documents to the next task."""
|
|
422
|
+
return self.output_shape in (OUTPUT_ONE_DOCUMENT, OUTPUT_DOCUMENT_PER_GRAPH)
|
|
423
|
+
|
|
424
|
+
@property
|
|
425
|
+
def _writes_into_graph(self) -> bool:
|
|
426
|
+
"""Whether this run puts the signature into the source graph."""
|
|
427
|
+
return self.output_shape == OUTPUT_INTO_GRAPH
|
|
428
|
+
|
|
429
|
+
def operation_description(self) -> str:
|
|
430
|
+
"""Describe what this run does to the graphs it processes, for the report."""
|
|
431
|
+
if not self.signing:
|
|
432
|
+
return "graphs hashed"
|
|
433
|
+
if self._writes_documents:
|
|
434
|
+
return "signature documents written"
|
|
435
|
+
return "graphs signed"
|
|
436
|
+
|
|
437
|
+
def _output_entity_schema(self) -> EntitySchema | None:
|
|
438
|
+
"""Return the schema this task emits, or None where it emits nothing.
|
|
439
|
+
|
|
440
|
+
Both the declared port and the returned Entities read it from here. Declaring
|
|
441
|
+
one schema and returning another is accepted by the task itself and rejected by
|
|
442
|
+
whatever is wired downstream, which is a confusing place to find out.
|
|
443
|
+
|
|
444
|
+
Writing into a graph of its own produces no output at all: the signature is in
|
|
445
|
+
that graph, and a port would only invite something to be wired to a result that
|
|
446
|
+
says nothing the graph does not already say.
|
|
447
|
+
"""
|
|
448
|
+
if self._writes_into_graph:
|
|
449
|
+
return None
|
|
450
|
+
return FileEntitySchema()
|
|
451
|
+
|
|
452
|
+
@staticmethod
|
|
453
|
+
def _resolve_secret(value: Password | str) -> str:
|
|
454
|
+
"""Return the decrypted string for a Password parameter, decrypted at point of use."""
|
|
455
|
+
return value if isinstance(value, str) else value.decrypt()
|
|
456
|
+
|
|
457
|
+
@classmethod
|
|
458
|
+
def _load_private_key(
|
|
459
|
+
cls, private_key: Password | str, passphrase: Password | str
|
|
460
|
+
) -> PrivateKeyTypes:
|
|
461
|
+
"""Load the configured private key, decrypting both secrets at the point of use."""
|
|
462
|
+
passphrase_str = cls._resolve_secret(passphrase)
|
|
463
|
+
return serialization.load_pem_private_key(
|
|
464
|
+
data=cls._resolve_secret(private_key).encode("utf-8"),
|
|
465
|
+
password=passphrase_str.encode("utf-8") if passphrase_str else None,
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
@staticmethod
|
|
469
|
+
def _public_key_is_multikey(private_key: PrivateKeyTypes) -> bool:
|
|
470
|
+
"""Whether the key of *private_key* may be typed sec:Multikey."""
|
|
471
|
+
return is_multikey(private_key.public_key())
|
|
472
|
+
|
|
473
|
+
@staticmethod
|
|
474
|
+
def _public_key_multibase(private_key: PrivateKeyTypes) -> str:
|
|
475
|
+
"""Return the multibase public key of *private_key*, for embedding in a proof.
|
|
476
|
+
|
|
477
|
+
An RSA private key carries its public key, so the key embedded in a proof is
|
|
478
|
+
always derived here rather than supplied separately - a key that does not
|
|
479
|
+
belong to the signature could only ever produce a proof that fails to verify.
|
|
480
|
+
"""
|
|
481
|
+
return public_key_multibase(private_key.public_key())
|
|
482
|
+
|
|
483
|
+
def create_key_pair(self) -> str:
|
|
484
|
+
"""Generate an Ed25519 key pair and return it for display, storing nothing.
|
|
485
|
+
|
|
486
|
+
Backs the *Create Ed25519 Key Pair* action.
|
|
487
|
+
"""
|
|
488
|
+
return self._key_pair_output(Ed25519PrivateKey.generate())
|
|
489
|
+
|
|
490
|
+
def create_rsa_key_pair(self) -> str:
|
|
491
|
+
"""Generate an RSA key pair and return it for display, storing nothing.
|
|
492
|
+
|
|
493
|
+
Backs the *Create RSA Key Pair* action.
|
|
494
|
+
"""
|
|
495
|
+
return self._key_pair_output(
|
|
496
|
+
generate_private_key(public_exponent=65537, key_size=GENERATED_RSA_KEY_SIZE)
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
def _key_pair_output(self, private_key: PrivateKeyTypes) -> str:
|
|
500
|
+
"""Render a freshly generated key pair as Markdown, storing nothing.
|
|
501
|
+
|
|
502
|
+
The keys are held in memory for the length of this call and returned as
|
|
503
|
+
text: nothing is written to disk, not even a temporary file, because a
|
|
504
|
+
private key belongs in a password store and in this task's encrypted
|
|
505
|
+
parameter, not in a file someone has to remember to delete.
|
|
506
|
+
"""
|
|
507
|
+
public_key = private_key.public_key()
|
|
508
|
+
passphrase = self._resolve_secret(self.private_key_passphrase)
|
|
509
|
+
encryption = (
|
|
510
|
+
serialization.BestAvailableEncryption(passphrase.encode("utf-8"))
|
|
511
|
+
if passphrase
|
|
512
|
+
else serialization.NoEncryption()
|
|
513
|
+
)
|
|
514
|
+
private_pem = private_key.private_bytes(
|
|
515
|
+
encoding=serialization.Encoding.PEM,
|
|
516
|
+
format=serialization.PrivateFormat.PKCS8,
|
|
517
|
+
encryption_algorithm=encryption,
|
|
518
|
+
).decode("utf-8")
|
|
519
|
+
public_pem = public_key.public_bytes(
|
|
520
|
+
encoding=serialization.Encoding.PEM,
|
|
521
|
+
format=serialization.PublicFormat.SubjectPublicKeyInfo,
|
|
522
|
+
).decode("utf-8")
|
|
523
|
+
protection = (
|
|
524
|
+
"encrypted with the passphrase configured on this task"
|
|
525
|
+
if passphrase
|
|
526
|
+
else "**not** encrypted - set a *Private Key Passphrase* first to encrypt it"
|
|
527
|
+
)
|
|
528
|
+
return (
|
|
529
|
+
"This key pair exists only in this message. It is stored nowhere, and it "
|
|
530
|
+
"cannot be shown again - copy both keys to your password store now.\n\n"
|
|
531
|
+
f"**Private key** ({protection}) - paste into *Private Key*:\n\n"
|
|
532
|
+
f"```\n{private_pem}```\n\n"
|
|
533
|
+
"**Public key** - paste into *Verify Graph Signature*'s *External Public "
|
|
534
|
+
f"Key*, where a verifier needs a key to trust:\n\n"
|
|
535
|
+
f"```\n{public_pem}```\n"
|
|
536
|
+
)
|
|
537
|
+
|
|
538
|
+
def _verification_method_iri(self, private_key: PrivateKeyTypes) -> str:
|
|
539
|
+
"""Return the verification method every proof of this run names.
|
|
540
|
+
|
|
541
|
+
The configured IRI where there is one, and otherwise the key's own did:key -
|
|
542
|
+
which ``__init__`` has already ensured exists. The specification requires a
|
|
543
|
+
proof to name its verification method, since that is how anyone else finds
|
|
544
|
+
the key to check it with.
|
|
545
|
+
"""
|
|
546
|
+
return self.verification_method_iri or verification_method_id(private_key.public_key())
|
|
547
|
+
|
|
548
|
+
def _own_signature(
|
|
549
|
+
self, graph: rdflib.Graph, private_key: PrivateKeyTypes, rdf_hash: str
|
|
550
|
+
) -> Callable[[rdflib.term.Node, rdflib.term.Node | None], bool]:
|
|
551
|
+
"""Return the test that tells this signer's own proofs from everybody else's.
|
|
552
|
+
|
|
553
|
+
A proof naming the verification method this run writes settles it outright:
|
|
554
|
+
that is the signer saying which key is theirs. So does a proof naming this
|
|
555
|
+
key's did:key, even where this run is configured to name the key another
|
|
556
|
+
way. Failing both, a proof this key made over the content as it is now is
|
|
557
|
+
one this key verifies.
|
|
558
|
+
"""
|
|
559
|
+
own_method = self._verification_method_iri(private_key)
|
|
560
|
+
public_key = private_key.public_key()
|
|
561
|
+
|
|
562
|
+
def is_own(proof_uri: rdflib.term.Node, vm_uri: rdflib.term.Node | None) -> bool:
|
|
563
|
+
if vm_uri is not None and (
|
|
564
|
+
str(vm_uri) == own_method or self._names_key(str(vm_uri), public_key)
|
|
565
|
+
):
|
|
566
|
+
return True
|
|
567
|
+
return self._made_by(graph, proof_uri, public_key, rdf_hash)
|
|
568
|
+
|
|
569
|
+
return is_own
|
|
570
|
+
|
|
571
|
+
@staticmethod
|
|
572
|
+
def _names_key(verification_method: str, public_key: PublicKeyTypes) -> bool:
|
|
573
|
+
"""Whether *verification_method* is a did:key encoding *public_key*."""
|
|
574
|
+
try:
|
|
575
|
+
named = public_key_from_did_key(verification_method)
|
|
576
|
+
except ValueError:
|
|
577
|
+
return False
|
|
578
|
+
return named is not None and named == public_key
|
|
579
|
+
|
|
580
|
+
@staticmethod
|
|
581
|
+
def _made_by(
|
|
582
|
+
graph: rdflib.Graph,
|
|
583
|
+
proof_uri: rdflib.term.Node,
|
|
584
|
+
public_key: PublicKeyTypes,
|
|
585
|
+
rdf_hash: str,
|
|
586
|
+
) -> bool:
|
|
587
|
+
"""Whether *public_key* made the signature stored on *graph* at *proof_uri*.
|
|
588
|
+
|
|
589
|
+
A proof states no digest of its own - the specification's proof has no such
|
|
590
|
+
property - so this can only be checked against the content as it is now. A
|
|
591
|
+
proof this key made over content that has since changed is still recognized
|
|
592
|
+
whenever it names the key, which every proof this task writes does.
|
|
593
|
+
"""
|
|
594
|
+
proof_value = str(next(graph.objects(proof_uri, SEC.proofValue), ""))
|
|
595
|
+
if not proof_value:
|
|
596
|
+
return False
|
|
597
|
+
try:
|
|
598
|
+
signature = multibase_decode(proof_value)
|
|
599
|
+
except (ValueError, TypeError):
|
|
600
|
+
# Proof material that will not decode is no signature this key can claim.
|
|
601
|
+
return False
|
|
602
|
+
options = signature_options(graph, proof_uri)
|
|
603
|
+
payload = proof_payload(canonical_digest(options), rdf_hash)
|
|
604
|
+
return verify_digest(public_key, payload, signature)
|
|
605
|
+
|
|
606
|
+
def _expires(self, created: str) -> str:
|
|
607
|
+
"""Return this run's proof expiry, formatted like *created*, or empty for none.
|
|
608
|
+
|
|
609
|
+
Computed from *created* rather than from now, so every proof in a run whose
|
|
610
|
+
signing spans more than an instant still expires a fixed number of days after
|
|
611
|
+
its own creation, not after the run's.
|
|
612
|
+
"""
|
|
613
|
+
if not self.proof_validity_days:
|
|
614
|
+
return ""
|
|
615
|
+
created_at = datetime.strptime(created, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=UTC)
|
|
616
|
+
expires_at = created_at + timedelta(days=self.proof_validity_days)
|
|
617
|
+
return expires_at.strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
618
|
+
|
|
619
|
+
def signed_proof(self, write: ProofWrite) -> rdflib.Graph:
|
|
620
|
+
"""Build the signed proof for one graph, as triples.
|
|
621
|
+
|
|
622
|
+
The same proof either goes into a graph of its own or leaves as a signature
|
|
623
|
+
document, so both are produced here and cannot drift apart.
|
|
624
|
+
"""
|
|
625
|
+
graph_iri, rdf_hash, private_key = write.graph_iri, write.rdf_hash, write.private_key
|
|
626
|
+
graph_uri_ref = URIRef(graph_iri)
|
|
627
|
+
embedded_key = self._public_key_multibase(private_key) if self.embed_public_key else ""
|
|
628
|
+
verification_method_iri = self._verification_method_iri(private_key)
|
|
629
|
+
metadata = ProofMetadata(
|
|
630
|
+
created=write.created,
|
|
631
|
+
verification_method_iri=verification_method_iri,
|
|
632
|
+
public_key_multibase=embedded_key,
|
|
633
|
+
cryptosuite=cryptosuite_for(private_key),
|
|
634
|
+
public_key_is_multikey=bool(embedded_key) and self._public_key_is_multikey(private_key),
|
|
635
|
+
expires=self._expires(write.created),
|
|
636
|
+
controller=did_key_controller(verification_method_iri),
|
|
637
|
+
)
|
|
638
|
+
# The proof is built twice: once with a placeholder to obtain its configuration -
|
|
639
|
+
# everything it asserts about itself, such as when it was created, its purpose
|
|
640
|
+
# and the verification method it names - and once for real, with a signature
|
|
641
|
+
# covering that configuration together with the document digest. Signing the
|
|
642
|
+
# digest alone would leave created, proofPurpose and verificationMethod
|
|
643
|
+
# editable after the fact.
|
|
644
|
+
proof_uri = new_proof_uri()
|
|
645
|
+
placeholder_proof = build_proof_rdf(
|
|
646
|
+
graph_uri=graph_uri_ref,
|
|
647
|
+
proof_uri=proof_uri,
|
|
648
|
+
signature_b64=_PROOF_VALUE_PLACEHOLDER,
|
|
649
|
+
metadata=metadata,
|
|
650
|
+
)
|
|
651
|
+
options = signature_options(placeholder_proof, proof_uri)
|
|
652
|
+
bound_signature = base64.b64encode(
|
|
653
|
+
sign_digest(private_key, proof_payload(canonical_digest(options), rdf_hash))
|
|
654
|
+
).decode("ascii")
|
|
655
|
+
return build_proof_rdf(
|
|
656
|
+
graph_uri=graph_uri_ref,
|
|
657
|
+
proof_uri=proof_uri,
|
|
658
|
+
signature_b64=bound_signature,
|
|
659
|
+
metadata=metadata,
|
|
660
|
+
)
|
|
661
|
+
|
|
662
|
+
def _write_proof(self, client: Client, write: ProofWrite, proof_rdf: rdflib.Graph) -> None:
|
|
663
|
+
"""Write this signer's proof into the data graph's companion proof graph.
|
|
664
|
+
|
|
665
|
+
The data graph itself plays no part in this write and is never downloaded,
|
|
666
|
+
reserialized or reimported here - only the companion proof graph is read and
|
|
667
|
+
written, holding every other signer's proof kept as it was, plus this one.
|
|
668
|
+
"""
|
|
669
|
+
graph_iri = write.graph_iri
|
|
670
|
+
graph_uri = URIRef(graph_iri)
|
|
671
|
+
existing = download_and_parse_proof_graph(client, graph_iri)
|
|
672
|
+
if existing is None:
|
|
673
|
+
existing = new_rdf_graph()
|
|
674
|
+
kept, replaced = proofs_by_other_keys(
|
|
675
|
+
existing,
|
|
676
|
+
graph_uri,
|
|
677
|
+
self._own_signature(existing, write.private_key, write.rdf_hash),
|
|
678
|
+
)
|
|
679
|
+
self.log.info(
|
|
680
|
+
f"{graph_iri}: {len(replaced)} own signature(s) replaced, "
|
|
681
|
+
f"{len(list(kept.subjects(RDF.type, SEC.DataIntegrityProof)))} kept"
|
|
682
|
+
)
|
|
683
|
+
combined = new_rdf_graph()
|
|
684
|
+
for triple in kept:
|
|
685
|
+
combined.add(triple)
|
|
686
|
+
for triple in proof_rdf:
|
|
687
|
+
combined.add(triple)
|
|
688
|
+
turtle_bytes = combined.serialize(format="turtle").encode("utf-8")
|
|
689
|
+
with tempfile.NamedTemporaryFile(suffix=".ttl", delete=False) as tmp:
|
|
690
|
+
tmp.write(turtle_bytes)
|
|
691
|
+
tmp_path = Path(tmp.name)
|
|
692
|
+
proof_graph_key = str(proof_graph_iri(graph_iri))
|
|
693
|
+
try:
|
|
694
|
+
client.graphs.import_item(
|
|
695
|
+
path=tmp_path,
|
|
696
|
+
key=proof_graph_key,
|
|
697
|
+
on_conflict=ImportConflictPolicy.REPLACE,
|
|
698
|
+
)
|
|
699
|
+
self.log.info(f"Proof written to proof graph: {proof_graph_key}")
|
|
700
|
+
finally:
|
|
701
|
+
Path(tmp_path).unlink(missing_ok=True)
|
|
702
|
+
|
|
703
|
+
def _entities(self, output_entities: list[Entity]) -> Entities | None:
|
|
704
|
+
"""Wrap the results, or return nothing where this task declares no port."""
|
|
705
|
+
schema = self._output_entity_schema()
|
|
706
|
+
if schema is None:
|
|
707
|
+
return None
|
|
708
|
+
return Entities(schema=schema, entities=iter(output_entities))
|
|
709
|
+
|
|
710
|
+
def _run_document(self, collected: rdflib.Graph, *, cancelled: bool) -> Entity | None:
|
|
711
|
+
"""Return the one document covering a whole run, if this run writes one.
|
|
712
|
+
|
|
713
|
+
A cancelled run writes none. A document covering the graphs signed before
|
|
714
|
+
the cancellation is indistinguishable from one covering every graph asked
|
|
715
|
+
for, and anyone verifying against it would see only passes.
|
|
716
|
+
"""
|
|
717
|
+
if self.output_shape != OUTPUT_ONE_DOCUMENT or len(collected) == 0:
|
|
718
|
+
return None
|
|
719
|
+
if cancelled:
|
|
720
|
+
self.log.warning("Run cancelled; no signature document written.")
|
|
721
|
+
return None
|
|
722
|
+
return FileEntitySchema().to_entity(self.signature_document("graph-signatures", collected))
|
|
723
|
+
|
|
724
|
+
def _document_directory(self) -> Path:
|
|
725
|
+
"""Return the directory this run writes its signature documents into.
|
|
726
|
+
|
|
727
|
+
One directory per run rather than one per document, registered for cleanup
|
|
728
|
+
so a long-lived worker does not accumulate a directory per graph signed.
|
|
729
|
+
"""
|
|
730
|
+
if self._documents_dir is None:
|
|
731
|
+
self._documents_dir = Path(tempfile.mkdtemp(prefix="graph-signatures-"))
|
|
732
|
+
return self._documents_dir
|
|
733
|
+
|
|
734
|
+
@staticmethod
|
|
735
|
+
def _digest_only(graph_iri: str, rdf_hash: str) -> rdflib.Graph:
|
|
736
|
+
"""Return the digest record for a graph that was hashed but not signed."""
|
|
737
|
+
return build_digest_record(URIRef(graph_iri), rdf_hash)
|
|
738
|
+
|
|
739
|
+
def signature_document(self, graph_iri: str, proof_rdf: rdflib.Graph) -> LocalFile:
|
|
740
|
+
"""Write one graph's signature to a JSON-LD document and return it as a file.
|
|
741
|
+
|
|
742
|
+
The document holds exactly the triples a proof written into a graph of its own
|
|
743
|
+
would, so the two say the same thing in two places.
|
|
744
|
+
"""
|
|
745
|
+
# The context is pinned to the two namespaces the document actually uses.
|
|
746
|
+
# rdflib would otherwise emit its entire default prefix table, and naming the
|
|
747
|
+
# W3C Data Integrity context here would claim a conformance this does not have.
|
|
748
|
+
jsonld = proof_rdf.serialize(
|
|
749
|
+
format="json-ld",
|
|
750
|
+
auto_compact=True,
|
|
751
|
+
context={"sec": str(SEC)},
|
|
752
|
+
)
|
|
753
|
+
slug = re.sub(r"^https?-+", "", re.sub(r"[^A-Za-z0-9]+", "-", graph_iri).strip("-"))
|
|
754
|
+
slug = slug[:100] or "graph"
|
|
755
|
+
path = self._document_directory() / f"{slug}.signature.json"
|
|
756
|
+
path.write_text(jsonld, encoding="utf-8")
|
|
757
|
+
return LocalFile(path=str(path), mime="application/ld+json")
|
|
758
|
+
|
|
759
|
+
def execute(
|
|
760
|
+
self,
|
|
761
|
+
inputs: Sequence[Entities],
|
|
762
|
+
context: ExecutionContext,
|
|
763
|
+
) -> Entities | None:
|
|
764
|
+
"""Run the workflow operator."""
|
|
765
|
+
self.log.info("Start creating graph hash.")
|
|
766
|
+
client = get_client(context)
|
|
767
|
+
|
|
768
|
+
# Collect graph IRIs from port 0; fall back to the input_graph parameter
|
|
769
|
+
graph_iris: list[str] = []
|
|
770
|
+
if inputs:
|
|
771
|
+
graph_iris.extend(
|
|
772
|
+
entity.values[0][0]
|
|
773
|
+
for entity in inputs[0].entities
|
|
774
|
+
if entity.values and entity.values[0]
|
|
775
|
+
)
|
|
776
|
+
if not graph_iris and self.input_graph:
|
|
777
|
+
graph_iris = [self.input_graph]
|
|
778
|
+
|
|
779
|
+
# Load the private key when one was supplied; that is what turns on signing
|
|
780
|
+
private_key = (
|
|
781
|
+
self._load_private_key(self.private_key, self.private_key_passphrase)
|
|
782
|
+
if self.signing
|
|
783
|
+
else None
|
|
784
|
+
)
|
|
785
|
+
|
|
786
|
+
# Process each graph
|
|
787
|
+
output_entities: list[Entity] = []
|
|
788
|
+
collected = new_rdf_graph()
|
|
789
|
+
created = datetime.now(UTC).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
790
|
+
|
|
791
|
+
cancelled = False
|
|
792
|
+
for processed, graph_iri in enumerate(graph_iris, start=1):
|
|
793
|
+
with suppress(AttributeError):
|
|
794
|
+
if context.workflow.status() == "Canceling":
|
|
795
|
+
cancelled = True
|
|
796
|
+
break
|
|
797
|
+
|
|
798
|
+
self.log.info(f"Processing graph: {graph_iri}")
|
|
799
|
+
full_graph = download_and_parse(
|
|
800
|
+
client=client, graph_iri=graph_iri, resolve_owl_imports=self.resolve_owl_imports
|
|
801
|
+
)
|
|
802
|
+
|
|
803
|
+
self.log.info(f"Graph size: {len(full_graph)}")
|
|
804
|
+
rdf_hash = hash_parsed_graph(full_graph)
|
|
805
|
+
self.log.info(rdf_hash)
|
|
806
|
+
|
|
807
|
+
proof_rdf = None
|
|
808
|
+
if private_key is not None:
|
|
809
|
+
write = ProofWrite(
|
|
810
|
+
graph_iri=graph_iri,
|
|
811
|
+
rdf_hash=rdf_hash,
|
|
812
|
+
private_key=private_key,
|
|
813
|
+
created=created,
|
|
814
|
+
)
|
|
815
|
+
proof_rdf = self.signed_proof(write)
|
|
816
|
+
if self._writes_into_graph:
|
|
817
|
+
self._write_proof(client, write, proof_rdf)
|
|
818
|
+
|
|
819
|
+
emitted = self._emit_for_graph(graph_iri, rdf_hash, proof_rdf, collected)
|
|
820
|
+
if emitted is not None:
|
|
821
|
+
output_entities.append(emitted)
|
|
822
|
+
|
|
823
|
+
context.report.update(
|
|
824
|
+
ExecutionReport(
|
|
825
|
+
entity_count=processed,
|
|
826
|
+
operation="write" if self._writes_into_graph else "read",
|
|
827
|
+
operation_desc=self.operation_description(),
|
|
828
|
+
)
|
|
829
|
+
)
|
|
830
|
+
|
|
831
|
+
run_document = self._run_document(collected, cancelled=cancelled)
|
|
832
|
+
if run_document is not None:
|
|
833
|
+
output_entities.append(run_document)
|
|
834
|
+
|
|
835
|
+
return self._entities(output_entities)
|