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/approval.py ADDED
@@ -0,0 +1,108 @@
1
+ # The approval: the person's own yes to one action, signed with the same
2
+ # passkey as the slip. Format description, section 17.
3
+
4
+ from . import _js
5
+ from . import fields as f
6
+ from .encoding import canonical_json, fingerprint, format_time, now_ms, random_id
7
+ from .jws import KINDS
8
+ from .slip import assemble_slip, prepare_passkey_record
9
+ from .stub import validate_request
10
+
11
+
12
+ def _truthy(value):
13
+ """Whether JavaScript counts a value as true: anything but nothing, false,
14
+ 0 and the empty text. An empty object or list counts as true."""
15
+ if value is None or value is False:
16
+ return False
17
+ if _js.is_number(value):
18
+ return value == value and value != 0
19
+ if isinstance(value, str):
20
+ return value != ''
21
+ return True
22
+
23
+
24
+ def _length(value):
25
+ """value.length in JavaScript: a text's UTF-16 units, a list's items, an
26
+ object's own member "length"; nothing for any other value."""
27
+ if isinstance(value, str):
28
+ return _js.utf16_length(value)
29
+ if isinstance(value, list):
30
+ return len(value)
31
+ if isinstance(value, dict):
32
+ return value.get('length')
33
+ return None
34
+
35
+
36
+ _ABSENT = object()
37
+ """undefined: a member that is not there. The canonical form refuses it."""
38
+
39
+
40
+ def _member(value, name):
41
+ """value[name] in JavaScript, for a value read from JSON: a member of an
42
+ object, and nothing for any other value. Nothing at all has no members."""
43
+ if value is None:
44
+ raise TypeError(f'Cannot read properties of null (reading {name!r})')
45
+ return value.get(name, _ABSENT) if isinstance(value, dict) else _ABSENT
46
+
47
+
48
+ def validate_approval_content(c):
49
+ """Confirm every member of an approval's content."""
50
+ f.members(c, ['action', 'id', 'slip', 'type', 'when'], ['amount', 'details', 'with'], 'approval')
51
+ f.id_(c['id'], 'id')
52
+ f.fingerprint_text(c['slip'], 'slip')
53
+ validate_request(c)
54
+ f.time(c['when'], 'when')
55
+
56
+
57
+ def request_of(c):
58
+ """What an approval and a stub must agree on: the action, and the amount,
59
+ the service and the documents, each present in both or in neither.
60
+
61
+ c: the content of an approval or of a stub. Returns text.
62
+ """
63
+ request = {'action': _member(c, 'action')}
64
+ for name in ('amount', 'details', 'with'):
65
+ if isinstance(c, dict) and name in c:
66
+ request[name] = c[name]
67
+ return canonical_json(request)
68
+
69
+
70
+ def prepare_approval(fields):
71
+ """Build the content of an approval and the challenge the person's passkey
72
+ must sign.
73
+
74
+ fields: "slip" (the fingerprint of the slip), "action", and where they
75
+ apply "amount" ({"unit", "value"}), "with", "details" ([{"name",
76
+ "sha256"}]) and "when" (milliseconds since 1970; defaults to now).
77
+ Returns {"contentBytes", "payloadB64", "protectedB64", "challenge",
78
+ "fingerprint"}: "fingerprint" is what the agent's stub must name as its
79
+ approval.
80
+ """
81
+ given_id = fields.get('id')
82
+ when = fields.get('when')
83
+ content = {
84
+ 'type': KINDS['approval']['type'],
85
+ 'id': random_id() if given_id is None else given_id,
86
+ 'slip': fields.get('slip'),
87
+ 'action': fields.get('action'),
88
+ 'when': format_time(now_ms() if when is None else when),
89
+ }
90
+ if _truthy(fields.get('amount')):
91
+ content['amount'] = fields['amount']
92
+ if _truthy(fields.get('with')):
93
+ content['with'] = fields['with']
94
+ details = fields.get('details')
95
+ if _truthy(details) and _truthy(_length(details)):
96
+ content['details'] = details
97
+ validate_approval_content(content)
98
+ prepared = prepare_passkey_record('approval', content)
99
+ return {**prepared, 'fingerprint': fingerprint(prepared['contentBytes'])}
100
+
101
+
102
+ def assemble_approval(prepared, authenticator_data, client_data_json, signature):
103
+ """Put the passkey's three values with the prepared content: the signed
104
+ approval. The values are stored exactly as the passkey returned them.
105
+
106
+ prepared: from prepare_approval. Returns the approval record.
107
+ """
108
+ return assemble_slip(prepared, authenticator_data, client_data_json, signature)
provared/blockstamp.py ADDED
@@ -0,0 +1,223 @@
1
+ # A block time-stamp: proof that a fingerprint was written into a block of
2
+ # a public blockchain. The chain is used purely as a clock. No coin, no
3
+ # wallet, no account and no payment is involved, in making a proof or in
4
+ # checking one.
5
+ #
6
+ # The proof is a file of the open format "OpenTimestamps", version 1, as
7
+ # that format's reference reader accepts it, read strictly here. It is a
8
+ # list of steps that lead from the fingerprint to the Merkle root in the
9
+ # header of a block of the Bitcoin blockchain: join bytes before, join
10
+ # bytes after, take SHA-256. The block's own 80-byte header comes with the
11
+ # proof.
12
+ #
13
+ # Nothing here rests on a signature. It rests on SHA-256 (FIPS 180-4), and
14
+ # on the person checking having named the block as one they trust to be
15
+ # part of the chain.
16
+ #
17
+ # Format description, section 21.4.
18
+
19
+ from .encoding import Refusal, format_time, sha256, to_base64url
20
+
21
+ MAX_PROOF_BYTES = 12288
22
+ """The largest proof accepted, in bytes: the same as for a time-stamp from a service."""
23
+
24
+ BLOCK_HEADER_BYTES = 80
25
+ """The length of a block header, in bytes."""
26
+
27
+ BLOCK_TIME_ALLOWANCE_MS = 7200 * 1000
28
+ """How far the time a block states may be from the true time: two hours.
29
+ A block's time is set by whoever made the block, within about that much."""
30
+
31
+ # The limits of the format's own reference reader.
32
+ _MAX_VALUE_BYTES = 4096
33
+ _MAX_ATTESTATION_BYTES = 8192
34
+ _MAX_DEPTH = 255
35
+
36
+ # How a proof file begins, and the mark of "this value is the Merkle root
37
+ # of a block of the Bitcoin blockchain".
38
+ _MAGIC = b'\x00' + b'OpenTimestamps' + b'\x00\x00' + b'Proof' + bytes([0x00, 0xBF, 0x89, 0xE2, 0xE8, 0x84, 0xE8, 0x92, 0x94])
39
+ _BLOCK_MARK = bytes([0x05, 0x88, 0x96, 0x0D, 0x73, 0xD7, 0x19, 0x01])
40
+
41
+ _SHA256 = 0x08
42
+ _JOIN_AFTER = 0xF0
43
+ _JOIN_BEFORE = 0xF1
44
+ _ATTESTATION = 0x00
45
+ _FORK = 0xFF
46
+
47
+
48
+ def _bad(why):
49
+ return Refusal('stamp-bad-data', f'A block time-stamp is not laid out as its format sets out: {why}')
50
+
51
+
52
+ class _Reader:
53
+ __slots__ = ('_data', '_at')
54
+
55
+ def __init__(self, data):
56
+ self._data = data
57
+ self._at = 0
58
+
59
+ def byte(self):
60
+ if self._at >= len(self._data):
61
+ raise _bad('it is cut short.')
62
+ b = self._data[self._at]
63
+ self._at += 1
64
+ return b
65
+
66
+ def take(self, length):
67
+ if self._at + length > len(self._data):
68
+ raise _bad('it is cut short.')
69
+ self._at += length
70
+ return self._data[self._at - length:self._at]
71
+
72
+ def done(self):
73
+ return self._at == len(self._data)
74
+
75
+
76
+ def _number(r, most):
77
+ """A whole number as the format writes it: seven bits to a byte, lowest
78
+ first, the top bit saying that another byte follows. Only the shortest
79
+ form is accepted, so that one number has one form."""
80
+ value = 0
81
+ for i in range(5):
82
+ b = r.byte()
83
+ value += (b & 0x7F) * 2 ** (7 * i)
84
+ if not (b & 0x80):
85
+ if i > 0 and b == 0:
86
+ raise _bad('a number is not written in its shortest form.')
87
+ if value > most:
88
+ raise _bad('a number is larger than the format allows.')
89
+ return value
90
+ raise _bad('a number is longer than the format allows.')
91
+
92
+
93
+ def _read_item(r, tag, value, found):
94
+ """One thing that follows from a value: a statement about it, or a step
95
+ that makes a new value. Returns the new value, or None after a statement."""
96
+ if tag == _ATTESTATION:
97
+ mark = r.take(8)
98
+ payload = r.take(_number(r, _MAX_ATTESTATION_BYTES))
99
+ # Statements of other kinds (a proof not yet complete, another chain) say nothing here.
100
+ if mark != _BLOCK_MARK:
101
+ return None
102
+ p = _Reader(payload)
103
+ height = _number(p, 2**35 - 1)
104
+ if not p.done():
105
+ raise _bad("the statement about a block holds more than the block's number.")
106
+ if len(value) == 32:
107
+ found.append({'root': value, 'height': height})
108
+ return None
109
+ if tag == _SHA256:
110
+ return sha256(value)
111
+ if tag == _JOIN_AFTER or tag == _JOIN_BEFORE:
112
+ joined = r.take(_number(r, _MAX_VALUE_BYTES))
113
+ if len(joined) == 0:
114
+ raise _bad('a step joins nothing.')
115
+ if len(value) + len(joined) > _MAX_VALUE_BYTES:
116
+ raise _bad('a step makes a value longer than 4,096 bytes.')
117
+ return value + joined if tag == _JOIN_AFTER else joined + value
118
+ raise _bad('it uses a step other than "join before", "join after" and SHA-256.')
119
+
120
+
121
+ def _read_from(r, value, depth, found):
122
+ """Everything that follows from a value. Each thing but the last is marked
123
+ as one of several. The JavaScript library reads this by calling itself;
124
+ here the same order of reading is kept with a list of the values whose
125
+ several things are not yet all read, so that a deep proof cannot
126
+ exhaust Python's own limit on calls."""
127
+ waiting = []
128
+ while True:
129
+ if depth > _MAX_DEPTH:
130
+ raise _bad('its steps go deeper than the format allows.')
131
+ tag = r.byte()
132
+ if tag == _FORK:
133
+ # One of several: after it, the next thing that follows from this value is read.
134
+ waiting.append((value, depth))
135
+ tag = r.byte()
136
+ following = _read_item(r, tag, value, found)
137
+ if following is not None:
138
+ value, depth = following, depth + 1
139
+ continue
140
+ if not waiting:
141
+ return
142
+ value, depth = waiting.pop()
143
+
144
+
145
+ def block_fingerprint(header):
146
+ """The fingerprint of a block, as it is written everywhere: SHA-256 taken
147
+ twice over the header, the bytes in reverse order, in hex (64 characters)."""
148
+ return sha256(sha256(bytes(header)))[::-1].hex()
149
+
150
+
151
+ def block_stamp_item(proof, header):
152
+ """Put a proof and the header of its block in the form they have beside a
153
+ seal or a cancellation, in the list "stamps".
154
+
155
+ proof: the proof file, complete: it must lead to a block. header: the
156
+ 80-byte header of that block. Returns {"block", "proof"}.
157
+ """
158
+ if not isinstance(proof, (bytes, bytearray)) or len(proof) == 0 or len(proof) > MAX_PROOF_BYTES:
159
+ raise _bad('it must be at most 12,288 bytes.')
160
+ if not isinstance(header, (bytes, bytearray)) or len(header) != BLOCK_HEADER_BYTES:
161
+ raise _bad('the block header that comes with it must be 80 bytes.')
162
+ return {'block': to_base64url(header), 'proof': to_base64url(proof)}
163
+
164
+
165
+ def check_block_stamp(proof, header, fingerprint_bytes, trusted=None):
166
+ """Check one block time-stamp.
167
+
168
+ proof: the proof file. header: the 80-byte header of the block the proof
169
+ leads to. fingerprint_bytes: the 32 bytes that must have been stamped.
170
+ trusted: the fingerprints (64 hex characters) of the blocks the checker
171
+ trusts to be part of the chain.
172
+
173
+ Returns {"kind", "state", "when", "time", "earliest", "authority",
174
+ "height"}. "valid": the proof leads to a block the checker named.
175
+ "untrusted": it leads to the block that comes with it, which the checker
176
+ did not name; never treat this as a time. "when" is the time the block
177
+ states. A block's time is only right to within about two hours, so there
178
+ are two times in milliseconds. "time" is when the record existed by: the
179
+ stated time and two hours. "earliest" is the earliest the proof could
180
+ have been made: the stated time less two hours. "authority" is the
181
+ block's fingerprint. "height" is the block's number as the proof states
182
+ it; nothing checks it.
183
+
184
+ Raises a Refusal: "stamp-bad-data", "stamp-wrong-data" or "stamp-invalid".
185
+ """
186
+ if not isinstance(proof, (bytes, bytearray)) or len(proof) > MAX_PROOF_BYTES:
187
+ raise _bad('it must be at most 12,288 bytes.')
188
+ if not isinstance(header, (bytes, bytearray)) or len(header) != BLOCK_HEADER_BYTES:
189
+ raise _bad('the block header that comes with it must be 80 bytes.')
190
+ proof = bytes(proof)
191
+ header = bytes(header)
192
+ r = _Reader(proof)
193
+ if r.take(len(_MAGIC)) != _MAGIC:
194
+ raise _bad('it does not begin as a proof file does.')
195
+ if r.byte() != 1:
196
+ raise _bad('it is not version 1 of the format.')
197
+ if r.byte() != _SHA256:
198
+ raise _bad('its first fingerprint is not SHA-256.')
199
+ stamped = r.take(32)
200
+ found = []
201
+ _read_from(r, stamped, 0, found)
202
+ if not r.done():
203
+ raise _bad('there are bytes after its end.')
204
+
205
+ if not isinstance(fingerprint_bytes, (bytes, bytearray)) or stamped != bytes(fingerprint_bytes):
206
+ raise Refusal('stamp-wrong-data', 'A block time-stamp was made for something other than this record.')
207
+ # The Merkle root is bytes 36 to 67 of a block header, and the time bytes 68 to 71, lowest first.
208
+ root = header[36:68]
209
+ match = next((f for f in found if f['root'] == root), None)
210
+ if match is None:
211
+ raise Refusal('stamp-invalid', 'A block time-stamp does not lead to the block that comes with it.')
212
+ stated = (header[68] + header[69] * 2**8 + header[70] * 2**16 + header[71] * 2**24) * 1000
213
+ fingerprint = block_fingerprint(header)
214
+ named = isinstance(trusted, list) and any(isinstance(t, str) and t.lower() == fingerprint for t in trusted)
215
+ return {
216
+ 'kind': 'block',
217
+ 'state': 'valid' if named else 'untrusted',
218
+ 'when': format_time(stated),
219
+ 'time': stated + BLOCK_TIME_ALLOWANCE_MS,
220
+ 'earliest': stated - BLOCK_TIME_ALLOWANCE_MS,
221
+ 'authority': fingerprint,
222
+ 'height': match['height'],
223
+ }