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