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