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/keys.py
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# Public keys: the exact shapes the format accepts (format description,
|
|
2
|
+
# section 3.8), their thumbprints (RFC 7638) and how each is loaded for
|
|
3
|
+
# checking a signature.
|
|
4
|
+
|
|
5
|
+
from . import _js
|
|
6
|
+
from .encoding import Refusal, from_base64url, sha256, to_base64url, utf8
|
|
7
|
+
|
|
8
|
+
KEY_SET_METHODS = ['Ed25519', 'ML-DSA-87']
|
|
9
|
+
"""The signing methods of an agent's or a service's key set, in order."""
|
|
10
|
+
|
|
11
|
+
SEAL_METHODS = ['Ed25519', 'ML-DSA-87', 'SLH-DSA-SHA2-256s']
|
|
12
|
+
"""The signing methods of a recorder's key set, which signs seals, in order.
|
|
13
|
+
The third rests only on fingerprint functions (FIPS 205)."""
|
|
14
|
+
|
|
15
|
+
PASSKEY_METHODS = ['ES256', 'RS256', 'Ed25519']
|
|
16
|
+
"""The signing methods a passkey may use."""
|
|
17
|
+
|
|
18
|
+
# For each method: the exact members of its JSON Web Key, in sorted order.
|
|
19
|
+
_SHAPES = {
|
|
20
|
+
'Ed25519': ['alg', 'crv', 'kty', 'x'],
|
|
21
|
+
'ML-DSA-87': ['alg', 'kty', 'pub'],
|
|
22
|
+
'SLH-DSA-SHA2-256s': ['alg', 'kty', 'pub'],
|
|
23
|
+
'ES256': ['alg', 'crv', 'kty', 'x', 'y'],
|
|
24
|
+
'RS256': ['alg', 'e', 'kty', 'n'],
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# The Ed25519 public keys under which anyone can make a signature that
|
|
29
|
+
# checks: the eight points of small order. RFC 8032 does not say to refuse
|
|
30
|
+
# them, and libraries differ, so a checker refuses them itself, with any
|
|
31
|
+
# key whose number y is not below p (written in more than one way), so
|
|
32
|
+
# that two checkers cannot disagree.
|
|
33
|
+
_P = 2**255 - 19
|
|
34
|
+
_ORDER_8_Y = 0x7A03AC9277FDC74EC6CC392CFA53202A0F67100D760B3CBA4FD84D3D706A17C7
|
|
35
|
+
_SMALL_ORDER_Y = frozenset((0, 1, _P - 1, _ORDER_8_Y, _P - _ORDER_8_Y))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def weak_ed25519(data):
|
|
39
|
+
"""Whether 32 bytes are an Ed25519 public key under which anyone can sign,
|
|
40
|
+
or one written in more than one way."""
|
|
41
|
+
y = int.from_bytes(bytes(data), 'little') & ((1 << 255) - 1)
|
|
42
|
+
return y >= _P or y in _SMALL_ORDER_Y
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _bad(where, why):
|
|
46
|
+
return Refusal('bad-key', f'{where}: {why}')
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _bytes_of(jwk, member, where):
|
|
50
|
+
try:
|
|
51
|
+
return from_base64url(jwk[member])
|
|
52
|
+
except Refusal:
|
|
53
|
+
raise _bad(where, f'"{member}" is not base64url.') from None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def check_key(jwk, allowed, where):
|
|
57
|
+
"""Confirm that a key is exactly one of the accepted shapes. Returns its method."""
|
|
58
|
+
if not isinstance(jwk, dict):
|
|
59
|
+
raise _bad(where, 'a key must be an object.')
|
|
60
|
+
alg = jwk.get('alg')
|
|
61
|
+
if not isinstance(alg, str) or alg not in allowed or alg not in _SHAPES:
|
|
62
|
+
raise _bad(where, f'the signing method must be one of {", ".join(allowed)}.')
|
|
63
|
+
names = _js.sort_strings(jwk.keys())
|
|
64
|
+
expected = _SHAPES[alg]
|
|
65
|
+
if names != expected:
|
|
66
|
+
raise _bad(where, f'a {alg} key must hold exactly {", ".join(expected)}.')
|
|
67
|
+
for n in names:
|
|
68
|
+
if not isinstance(jwk[n], str):
|
|
69
|
+
raise _bad(where, f'"{n}" must be text.')
|
|
70
|
+
if alg == 'Ed25519':
|
|
71
|
+
if jwk['kty'] != 'OKP' or jwk['crv'] != 'Ed25519':
|
|
72
|
+
raise _bad(where, 'an Ed25519 key has kty OKP and crv Ed25519.')
|
|
73
|
+
x = _bytes_of(jwk, 'x', where)
|
|
74
|
+
if len(x) != 32:
|
|
75
|
+
raise _bad(where, 'an Ed25519 public key is 32 bytes.')
|
|
76
|
+
if weak_ed25519(x):
|
|
77
|
+
raise _bad(where, 'this Ed25519 public key is one under which anyone can sign, and is not accepted.')
|
|
78
|
+
elif alg == 'ML-DSA-87':
|
|
79
|
+
if jwk['kty'] != 'AKP':
|
|
80
|
+
raise _bad(where, 'an ML-DSA-87 key has kty AKP.')
|
|
81
|
+
# FIPS 204, table 2: an ML-DSA-87 public key is 2,592 bytes.
|
|
82
|
+
if len(_bytes_of(jwk, 'pub', where)) != 2592:
|
|
83
|
+
raise _bad(where, 'an ML-DSA-87 public key is 2,592 bytes.')
|
|
84
|
+
elif alg == 'SLH-DSA-SHA2-256s':
|
|
85
|
+
if jwk['kty'] != 'AKP':
|
|
86
|
+
raise _bad(where, 'an SLH-DSA key has kty AKP.')
|
|
87
|
+
# FIPS 205, table 2: an SLH-DSA-SHA2-256s public key is 64 bytes.
|
|
88
|
+
if len(_bytes_of(jwk, 'pub', where)) != 64:
|
|
89
|
+
raise _bad(where, 'an SLH-DSA-SHA2-256s public key is 64 bytes.')
|
|
90
|
+
elif alg == 'ES256':
|
|
91
|
+
if jwk['kty'] != 'EC' or jwk['crv'] != 'P-256':
|
|
92
|
+
raise _bad(where, 'an ES256 key has kty EC and crv P-256.')
|
|
93
|
+
if len(_bytes_of(jwk, 'x', where)) != 32 or len(_bytes_of(jwk, 'y', where)) != 32:
|
|
94
|
+
raise _bad(where, 'the two halves of a P-256 public key are 32 bytes each.')
|
|
95
|
+
else:
|
|
96
|
+
if jwk['kty'] != 'RSA' or jwk['e'] != 'AQAB':
|
|
97
|
+
raise _bad(where, 'an RS256 key has kty RSA and e AQAB.')
|
|
98
|
+
n = _bytes_of(jwk, 'n', where)
|
|
99
|
+
# The size in bits: whole bytes after the first, and the bits of the first.
|
|
100
|
+
bits = 0 if len(n) == 0 or n[0] == 0 else (len(n) - 1) * 8 + n[0].bit_length()
|
|
101
|
+
if bits < 2048 or bits > 8192:
|
|
102
|
+
raise _bad(where, 'an RS256 key is 2,048 to 8,192 bits.')
|
|
103
|
+
return alg
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def check_key_set(keys, where):
|
|
107
|
+
"""Confirm that a value is a key set: exactly an Ed25519 key, then an
|
|
108
|
+
ML-DSA-87 key (format description, section 3.9)."""
|
|
109
|
+
if not isinstance(keys, list) or len(keys) != len(KEY_SET_METHODS):
|
|
110
|
+
raise _bad(where, 'a key set is exactly two keys: Ed25519, then ML-DSA-87.')
|
|
111
|
+
for i, alg in enumerate(KEY_SET_METHODS):
|
|
112
|
+
check_key(keys[i], [alg], f'{where}[{i}]')
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def check_seal_key_set(keys, where):
|
|
116
|
+
"""Confirm that a value is a recorder's key set: exactly an Ed25519 key, an
|
|
117
|
+
ML-DSA-87 key and an SLH-DSA-SHA2-256s key, in that order."""
|
|
118
|
+
if not isinstance(keys, list) or len(keys) != len(SEAL_METHODS):
|
|
119
|
+
raise _bad(where, "a recorder's key set is exactly three keys: Ed25519, ML-DSA-87, then SLH-DSA-SHA2-256s.")
|
|
120
|
+
for i, alg in enumerate(SEAL_METHODS):
|
|
121
|
+
check_key(keys[i], [alg], f'{where}[{i}]')
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def thumbprint(jwk):
|
|
125
|
+
"""The thumbprint of a key: RFC 7638 with SHA-256. The members used are those
|
|
126
|
+
RFC 7638 section 3.2 names, and for ML-DSA those RFC 9964 section 6 names.
|
|
127
|
+
The key must already have passed check_key."""
|
|
128
|
+
required = dict(jwk)
|
|
129
|
+
# "alg" is part of the thumbprint only for the AKP key type.
|
|
130
|
+
if jwk.get('kty') != 'AKP':
|
|
131
|
+
required.pop('alg', None)
|
|
132
|
+
names = _js.sort_strings(required.keys())
|
|
133
|
+
text = '{' + ','.join(_js.string(n) + ':' + _js.string(required[n]) for n in names) + '}'
|
|
134
|
+
return to_base64url(sha256(utf8(text)))
|
provared/pass_.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Passing a slip on: an agent hands part of its permission to a helper
|
|
2
|
+
# agent. Format description, section 25.
|
|
3
|
+
#
|
|
4
|
+
# The module is named "pass_" because "pass" is a word of Python itself.
|
|
5
|
+
|
|
6
|
+
from . import _js
|
|
7
|
+
from . import fields as f
|
|
8
|
+
from .encoding import format_time, now_ms, random_id
|
|
9
|
+
from .jws import KINDS
|
|
10
|
+
from .keys import check_key_set
|
|
11
|
+
from .slip import validate_limits
|
|
12
|
+
from .stub import sign_with_key_set
|
|
13
|
+
|
|
14
|
+
MAX_PASSES = 10
|
|
15
|
+
"""The most times a slip may let its permission be passed on."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _truthy(value):
|
|
19
|
+
"""Whether JavaScript counts a value as true: anything but nothing, false,
|
|
20
|
+
0 and the empty text. An empty object or list counts as true."""
|
|
21
|
+
if value is None or value is False:
|
|
22
|
+
return False
|
|
23
|
+
if _js.is_number(value):
|
|
24
|
+
return value == value and value != 0
|
|
25
|
+
if isinstance(value, str):
|
|
26
|
+
return value != ''
|
|
27
|
+
return True
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def validate_pass_content(c):
|
|
31
|
+
"""Confirm every member of a pass's content."""
|
|
32
|
+
f.members(c, ['actions', 'id', 'limits', 'slip', 'to', 'type', 'validFrom', 'validUntil', 'when'], ['from'], 'pass')
|
|
33
|
+
f.id_(c['id'], 'id')
|
|
34
|
+
f.fingerprint_text(c['slip'], 'slip')
|
|
35
|
+
if 'from' in c:
|
|
36
|
+
f.fingerprint_text(c['from'], 'from')
|
|
37
|
+
f.members(c['to'], ['keys', 'name'], [], 'to')
|
|
38
|
+
f.label(c['to']['name'], 'to.name')
|
|
39
|
+
check_key_set(c['to']['keys'], 'to.keys')
|
|
40
|
+
f.list_of(c['actions'], 1, 64, 'actions')
|
|
41
|
+
for i, a in enumerate(c['actions']):
|
|
42
|
+
f.action_name(a, f'actions[{i}]')
|
|
43
|
+
if len(set(c['actions'])) != len(c['actions']):
|
|
44
|
+
raise f.fail('actions', 'an action name is repeated.')
|
|
45
|
+
validate_limits(c['actions'], c['limits'])
|
|
46
|
+
start = f.time(c['validFrom'], 'validFrom')
|
|
47
|
+
until = f.time(c['validUntil'], 'validUntil')
|
|
48
|
+
if not (start < until):
|
|
49
|
+
raise f.fail('validUntil', 'must be later than validFrom.')
|
|
50
|
+
f.time(c['when'], 'when')
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def write_pass(fields, private_keys):
|
|
54
|
+
"""Write and sign a pass: the agent that holds a permission hands part of
|
|
55
|
+
it to a helper agent.
|
|
56
|
+
|
|
57
|
+
fields: "slip" (the fingerprint of the slip); "from" (the fingerprint of
|
|
58
|
+
the pass the signer itself acts under; absent when the signer is the
|
|
59
|
+
slip's own agent); "to" (the helper agent's public key set and its name,
|
|
60
|
+
a label: {"keys", "name"}); "actions" (what the helper may do: no more
|
|
61
|
+
than the signer may); "limits" (limits for the helper, in the form a slip
|
|
62
|
+
uses; defaults to none); "validFrom", "validUntil" and "when"
|
|
63
|
+
(milliseconds since 1970; defaults to now).
|
|
64
|
+
private_keys: the private keys of the agent that passes on, Ed25519 then
|
|
65
|
+
ML-DSA-87.
|
|
66
|
+
Returns {"record", "fingerprint"}.
|
|
67
|
+
"""
|
|
68
|
+
given_id = fields.get('id')
|
|
69
|
+
limits = fields.get('limits')
|
|
70
|
+
when = fields.get('when')
|
|
71
|
+
content = {
|
|
72
|
+
'type': KINDS['pass']['type'],
|
|
73
|
+
'id': random_id() if given_id is None else given_id,
|
|
74
|
+
'slip': fields.get('slip'),
|
|
75
|
+
'to': fields.get('to'),
|
|
76
|
+
'actions': fields.get('actions'),
|
|
77
|
+
'limits': [] if limits is None else limits,
|
|
78
|
+
'validFrom': fields.get('validFrom'),
|
|
79
|
+
'validUntil': fields.get('validUntil'),
|
|
80
|
+
'when': format_time(now_ms() if when is None else when),
|
|
81
|
+
}
|
|
82
|
+
if _truthy(fields.get('from')):
|
|
83
|
+
content['from'] = fields['from']
|
|
84
|
+
validate_pass_content(content)
|
|
85
|
+
record, fp = sign_with_key_set('pass', content, private_keys)
|
|
86
|
+
return {'record': record, 'fingerprint': fp}
|