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/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