provared 0.2.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.
- provared/__init__.py +60 -0
- provared/_js.py +450 -0
- provared/actions.py +84 -0
- provared/approval.py +108 -0
- provared/blockstamp.py +223 -0
- provared/book.py +2320 -0
- provared/check.py +21 -0
- provared/compare.py +266 -0
- provared/cover.py +187 -0
- provared/der.py +230 -0
- provared/encoding.py +278 -0
- provared/fields.py +95 -0
- provared/guard.py +177 -0
- provared/headers.py +507 -0
- provared/jws.py +164 -0
- provared/keys.py +134 -0
- provared/pass_.py +86 -0
- provared/recorder.py +1426 -0
- provared/seal.py +133 -0
- provared/service.py +138 -0
- provared/signatures.py +149 -0
- provared/slip.py +413 -0
- provared/standing.py +198 -0
- provared/stub.py +131 -0
- provared/timestamp.py +456 -0
- provared/tools.py +415 -0
- provared/tree.py +148 -0
- provared/webauthn.py +107 -0
- provared-0.2.0.dist-info/METADATA +176 -0
- provared-0.2.0.dist-info/RECORD +34 -0
- provared-0.2.0.dist-info/WHEEL +5 -0
- provared-0.2.0.dist-info/licenses/LICENSE +202 -0
- provared-0.2.0.dist-info/licenses/LICENSE-CC-BY-4.0.txt +396 -0
- provared-0.2.0.dist-info/top_level.txt +1 -0
provared/guard.py
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# The check before acting: would this action be outside the slip?
|
|
2
|
+
#
|
|
3
|
+
# The software around an agent can ask this before each action, and stop the
|
|
4
|
+
# action when the answer is no. It uses the same comparison as the checker
|
|
5
|
+
# (compare.py), so nothing is allowed here that the checker would report
|
|
6
|
+
# afterwards. It refuses more in one case only: an action that would add to
|
|
7
|
+
# a period which a stub dated after it already overfills. It writes
|
|
8
|
+
# nothing and changes nothing.
|
|
9
|
+
|
|
10
|
+
from collections.abc import Mapping
|
|
11
|
+
|
|
12
|
+
from . import fields as f
|
|
13
|
+
from .book import MAX_ENTRIES, MAX_LINE_BYTES, _Ids, check_approval_record, check_book_keeping_state, fixed_options, keys_of, pass_as_slip
|
|
14
|
+
from .compare import compare_with_slip
|
|
15
|
+
from .encoding import Refusal, canonical_json, format_time, nan_time, now_ms, problem_from, utf8
|
|
16
|
+
from .stub import validate_request
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _not_sound(why):
|
|
20
|
+
return Refusal('record-not-sound', why)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# An upper bound on the line that the record of an action will make: the
|
|
24
|
+
# stub with its two signatures, the approval that goes with it, and the
|
|
25
|
+
# other side's countersignature. A line of a book may not be longer than
|
|
26
|
+
# 131,072 bytes, and an action whose record would not fit is not allowed.
|
|
27
|
+
_STUB_BOUND = 6700 + 600
|
|
28
|
+
_COUNTERSIGNATURE_BOUND = 6900
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _line_bound(c, approval, has_approval):
|
|
32
|
+
approval_bytes = 0
|
|
33
|
+
if has_approval:
|
|
34
|
+
try:
|
|
35
|
+
approval_bytes = len(utf8(canonical_json(approval)))
|
|
36
|
+
except Exception:
|
|
37
|
+
approval_bytes = 0 # not a record at all: its own check refuses it
|
|
38
|
+
return -(-(len(utf8(canonical_json(c))) * 4) // 3) + _STUB_BOUND + approval_bytes + _COUNTERSIGNATURE_BOUND + 64
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def check_before(book, proposal, options=None, **named):
|
|
42
|
+
"""Ask whether an action the agent is about to take would be outside its slip.
|
|
43
|
+
|
|
44
|
+
book: the book so far: the slip and every stub written under it.
|
|
45
|
+
proposal: "slip" (the fingerprint of the slip), "action", and where they
|
|
46
|
+
apply "amount" ({"unit", "value"}), "with" (the id of the service, as the
|
|
47
|
+
slip names it), "details", "approval" (the person's signed approval of
|
|
48
|
+
exactly this action, where the slip asks for one), "terms" (the
|
|
49
|
+
fingerprint of the service's terms for agents, if they are in the book),
|
|
50
|
+
"pass" (for a helper agent: the fingerprint of the pass it acts under),
|
|
51
|
+
"when" (milliseconds since 1970; defaults to now).
|
|
52
|
+
options: the same options as check_book. issuer_keys (the thumbprints of
|
|
53
|
+
the passkeys this check trusts) must be given: a slip signed by any
|
|
54
|
+
other key allows nothing.
|
|
55
|
+
|
|
56
|
+
Returns {"allowed", "problems", "breaches", "needs"}. "allowed" is True
|
|
57
|
+
only if the record so far is sound, this device checked every signature
|
|
58
|
+
in it, and the action would be inside the slip. "needs" lists what the
|
|
59
|
+
slip asks to go with the action. A needed approval must be handed over,
|
|
60
|
+
or the action is not allowed. A needed countersignature cannot be known
|
|
61
|
+
beforehand: the action is allowed, and is inside the slip only if the
|
|
62
|
+
other side then countersigns it.
|
|
63
|
+
"""
|
|
64
|
+
# The options are read once, through the fixed copy a whole check reads
|
|
65
|
+
# them through, for the book and for the decision alike.
|
|
66
|
+
fixed = fixed_options(options, **named)
|
|
67
|
+
return _decide(lambda: check_book_keeping_state(book, fixed), proposal, fixed)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def check_before_read(read, proposal, options=None):
|
|
71
|
+
"""The same check, for a book that has already been read and is carried on
|
|
72
|
+
(the stub writer): nothing is read again. Not part of the public interface."""
|
|
73
|
+
return _decide(read, proposal, options)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _decide(read, proposal, options):
|
|
77
|
+
answer = {'allowed': False, 'problems': [], 'breaches': [], 'needs': []}
|
|
78
|
+
try:
|
|
79
|
+
if not isinstance(options, Mapping):
|
|
80
|
+
options = {}
|
|
81
|
+
issuer_keys = options.get('issuerKeys')
|
|
82
|
+
if not isinstance(issuer_keys, list) or len(issuer_keys) == 0:
|
|
83
|
+
raise _not_sound('The check before acting must be told which passkeys it trusts. A slip signed by any other key allows nothing.')
|
|
84
|
+
got = read()
|
|
85
|
+
result, state, beside = got['result'], got['state'], got['beside']
|
|
86
|
+
if result['summary']['problemFound']:
|
|
87
|
+
raise _not_sound('A problem was found in the record so far. It must be looked at before the agent acts again.')
|
|
88
|
+
# A record this device could only partly check is not a pass, here either.
|
|
89
|
+
if not result['summary']['fullyChecked']:
|
|
90
|
+
raise _not_sound('This device could not check every signature in the record so far.')
|
|
91
|
+
if not all(e.get('compared') is True for e in result['entries'] if e['kind'] in ('stub', 'pass')):
|
|
92
|
+
raise _not_sound('This device could not compare every stub and every pass in the record so far with its slip.')
|
|
93
|
+
if result['size'] >= MAX_ENTRIES:
|
|
94
|
+
raise Refusal('too-large', 'The book is full: it holds 100,000 entries.')
|
|
95
|
+
|
|
96
|
+
f.members(proposal, ['action', 'slip'], ['amount', 'approval', 'details', 'pass', 'terms', 'when', 'with'], 'proposal')
|
|
97
|
+
when_given = proposal.get('when')
|
|
98
|
+
c = {'action': proposal['action'], 'when': format_time(now_ms() if when_given is None else when_given)}
|
|
99
|
+
for name in ('amount', 'details', 'with'):
|
|
100
|
+
if name in proposal:
|
|
101
|
+
c[name] = proposal[name]
|
|
102
|
+
validate_request(c)
|
|
103
|
+
when = f.time(c['when'], 'when')
|
|
104
|
+
if _line_bound(c, proposal.get('approval'), 'approval' in proposal) > MAX_LINE_BYTES:
|
|
105
|
+
raise Refusal('too-large', 'The record of this action would be longer than a line of a book may be.')
|
|
106
|
+
|
|
107
|
+
slip = state['slips'].get(proposal['slip']) if isinstance(proposal['slip'], str) else None
|
|
108
|
+
if not slip:
|
|
109
|
+
raise Refusal('slip-missing', 'The slip is not in the book.')
|
|
110
|
+
if not slip['verified']:
|
|
111
|
+
raise _not_sound("This device could not check the slip's signature.")
|
|
112
|
+
# A helper agent acts under a pass, in a chain of its own.
|
|
113
|
+
p = None
|
|
114
|
+
if 'pass' in proposal:
|
|
115
|
+
p = state['passes'].get(proposal['pass']) if isinstance(proposal['pass'], str) else None
|
|
116
|
+
if not p or not p['usable']:
|
|
117
|
+
raise Refusal('pass-missing', 'The pass is not in the book, or did not pass its own check.')
|
|
118
|
+
if p['content']['slip'] != proposal['slip']:
|
|
119
|
+
raise Refusal('pass-mismatch', 'The pass was given under another slip.')
|
|
120
|
+
# The chain allows no stub dated before the one ahead of it.
|
|
121
|
+
if when < (p if p else slip)['lastWhen']:
|
|
122
|
+
raise Refusal('time-went-backwards', 'The action is dated before the last stub in its chain.')
|
|
123
|
+
|
|
124
|
+
service = next((s for s in slip['content']['with'] if s['id'] == c['with']), None) if 'with' in c else None
|
|
125
|
+
terms = None
|
|
126
|
+
if 'terms' in proposal:
|
|
127
|
+
t = state['terms'].get(proposal['terms']) if isinstance(proposal['terms'], str) else None
|
|
128
|
+
if not t or not t['usable'] or not t['verified']:
|
|
129
|
+
raise Refusal('terms-not-found', 'The terms are not in the book, or did not pass their check.')
|
|
130
|
+
if not keys_of(service) or canonical_json(service['keys']) != canonical_json(t['content']['by']['keys']):
|
|
131
|
+
raise Refusal('terms-mismatch', 'The terms were not signed with the keys the slip gives for the service named.')
|
|
132
|
+
terms = t['content']
|
|
133
|
+
|
|
134
|
+
# Cancelled in the book, or by a cancellation that the person kept and handed over beside it.
|
|
135
|
+
cancelled = slip['cancelled'] is not None or proposal['slip'] in beside
|
|
136
|
+
shown = compare_with_slip(slip['content'], slip['tally'], c, {'service': service, 'terms': terms, 'cancelled': cancelled}, False)
|
|
137
|
+
answer['needs'] = shown['needs']
|
|
138
|
+
breaches = list(shown['breaches'])
|
|
139
|
+
if p:
|
|
140
|
+
# The pass the helper acts under, and every pass above it, as the checker compares them.
|
|
141
|
+
q = p
|
|
142
|
+
while q:
|
|
143
|
+
under = compare_with_slip(pass_as_slip(q['content']), q['tally'], c, {'service': service, 'what': 'pass' if q is p else 'earlier-pass'}, False)
|
|
144
|
+
for b in under['breaches']:
|
|
145
|
+
if not any(x['code'] == b['code'] for x in breaches):
|
|
146
|
+
breaches.append(b)
|
|
147
|
+
q = q['parent']
|
|
148
|
+
if p['notAllowed']:
|
|
149
|
+
breaches.append({'code': 'pass-not-allowed', 'message': 'The slip does not allow the pass this action would be taken under.'})
|
|
150
|
+
|
|
151
|
+
# The person's approval, checked as the checker will check it.
|
|
152
|
+
if 'approval' in proposal:
|
|
153
|
+
ids = _Ids(state['ids'])
|
|
154
|
+
ids.approvals = set(getattr(state['ids'], 'approvals', ()))
|
|
155
|
+
without = options.get('withoutMethods') if isinstance(options.get('withoutMethods'), list) else []
|
|
156
|
+
approval = check_approval_record(proposal['approval'], c, proposal['slip'], slip, ids, without)
|
|
157
|
+
if approval['state'] != 'valid':
|
|
158
|
+
raise _not_sound("This device could not check the approval's signature.")
|
|
159
|
+
if nan_time(approval['when']) > when + 300 * 1000:
|
|
160
|
+
raise Refusal('approval-dated-after-stub', 'The approval is dated later than the action.')
|
|
161
|
+
elif 'approval' in shown['needs']:
|
|
162
|
+
breaches.append({'code': 'approval-missing', 'message': "The slip asks for the person's own approval of this action, and none was handed to this check."})
|
|
163
|
+
# A countersignature cannot be known beforehand. But where the action
|
|
164
|
+
# names no service, or one for which the slip gives no keys, none can
|
|
165
|
+
# ever be given.
|
|
166
|
+
if 'countersignature' in shown['needs'] and not keys_of(service):
|
|
167
|
+
breaches.append({
|
|
168
|
+
'code': 'countersignature-missing',
|
|
169
|
+
'message': 'The slip asks for the other side to countersign this action, and the action names no service whose keys the slip gives. '
|
|
170
|
+
'No countersignature could be given.',
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
answer['breaches'] = breaches
|
|
174
|
+
answer['allowed'] = len(breaches) == 0
|
|
175
|
+
except Exception as e:
|
|
176
|
+
answer['problems'].append(problem_from(e))
|
|
177
|
+
return answer
|