246 lines
11 KiB
Python
246 lines
11 KiB
Python
"""Shared accepted D09 grammar; transport authentication belongs to the caller.
|
|
|
|
Installed beside both fixed entrypoints in /usr/libexec. The schema is pinned,
|
|
not selectable by a requester. No test-only contract checker is imported.
|
|
"""
|
|
import hashlib
|
|
import json
|
|
import pathlib
|
|
import re
|
|
|
|
SCHEMA_SHA256 = '2e293bf7481c8442b40521a5960467fc4a83913ff5d8a3832b05aa6e49d23839'
|
|
MAX = 65536
|
|
|
|
|
|
def canonical(value):
|
|
return json.dumps(value, sort_keys=True, separators=(',', ':'), ensure_ascii=True,
|
|
allow_nan=False).encode('ascii')
|
|
|
|
|
|
def digest(raw):
|
|
return hashlib.sha256(raw).hexdigest()
|
|
|
|
|
|
def release_digest(identity):
|
|
"""Archive domain, deliberately distinct from protected object_digest.
|
|
|
|
The external admission attests the mapping of these downloaded components
|
|
to objects.new (including runtime extraction and preserved frontend assets).
|
|
URLs, mirror choice and the downloader's current version are not identity.
|
|
"""
|
|
if type(identity) is not list or len(identity) != 2:
|
|
raise ValueError('release identity')
|
|
version, components = identity
|
|
def token(value):
|
|
return type(value) is str and re.fullmatch('[A-Za-z0-9_.+-]{1,128}', value)
|
|
if not token(version) or type(components) is not list or not 1 <= len(components) <= 16:
|
|
raise ValueError('release identity bounds')
|
|
names = []
|
|
for item in components:
|
|
if type(item) is not list or len(item) != 4:
|
|
raise ValueError('component identity')
|
|
name, version, hashed, size = item
|
|
if not token(name) or not token(version) or type(hashed) is not str or not re.fullmatch('[0-9a-f]{64}', hashed) or type(size) is not int or not 0 < size <= 9007199254740991:
|
|
raise ValueError('component identity fields')
|
|
names.append(name)
|
|
if names != sorted(set(names)):
|
|
raise ValueError('component ordering/duplicates')
|
|
return digest(canonical(identity))
|
|
|
|
|
|
def pairs(items):
|
|
result = {}
|
|
for key, value in items:
|
|
if key in result:
|
|
raise ValueError('duplicate field')
|
|
result[key] = value
|
|
return result
|
|
|
|
|
|
def reject(value):
|
|
raise ValueError('noncanonical number')
|
|
|
|
|
|
def parse(raw):
|
|
if not raw or len(raw) > MAX:
|
|
raise ValueError('wire size')
|
|
try:
|
|
value = json.loads(raw.decode('ascii'), object_pairs_hook=pairs,
|
|
parse_float=reject, parse_constant=reject)
|
|
except RecursionError as exc:
|
|
raise ValueError('wire depth') from exc
|
|
if canonical(value) != raw:
|
|
raise ValueError('noncanonical bytes')
|
|
def depth(item, level=1):
|
|
if level > 8:
|
|
raise ValueError('wire depth')
|
|
if isinstance(item, dict):
|
|
for child in item.values():
|
|
depth(child, level + 1)
|
|
elif isinstance(item, list):
|
|
for child in item:
|
|
depth(child, level + 1)
|
|
depth(value)
|
|
return value
|
|
|
|
|
|
def schema():
|
|
raw = pathlib.Path(__file__).with_name('d09-wire-schema.json').read_bytes()
|
|
if digest(raw) != SCHEMA_SHA256:
|
|
raise ValueError('unreviewed D09 schema')
|
|
return json.loads(raw)
|
|
|
|
|
|
def validate(record, value):
|
|
grammar = schema()
|
|
def check(rule, item):
|
|
if isinstance(rule, str):
|
|
rule = grammar['definitions'][rule]
|
|
kind = rule['kind']
|
|
if kind == 'record':
|
|
validate(rule['of'], item)
|
|
elif kind == 'object':
|
|
optional = rule.get('optional_fields', {})
|
|
if (type(item) is not dict or not set(rule['exact_fields']) <= set(item)
|
|
or not set(item) <= set(rule['exact_fields']) | set(optional)):
|
|
raise ValueError('exact fields')
|
|
for key, subrule in rule['exact_fields'].items():
|
|
check(subrule, item[key])
|
|
for key, subrule in optional.items():
|
|
if key in item:
|
|
check(subrule, item[key])
|
|
elif kind == 'boolean':
|
|
if type(item) is not bool:
|
|
raise ValueError('boolean')
|
|
elif kind == 'string':
|
|
if type(item) is not str or not 0 < len(item) <= rule['maximum'] or not all(32 <= ord(c) <= 126 for c in item):
|
|
raise ValueError('bounded text')
|
|
elif kind == 'nullable':
|
|
if item is not None:
|
|
check(rule['of'], item)
|
|
elif kind == 'integer':
|
|
if type(item) is not int or not rule['minimum'] <= item <= rule['maximum']:
|
|
raise ValueError('bounded integer')
|
|
elif kind == 'constant':
|
|
if type(item) is not type(rule['value']) or item != rule['value']:
|
|
raise ValueError('constant')
|
|
elif kind == 'enum':
|
|
if type(item) is not str or item not in rule['values']:
|
|
raise ValueError('enum')
|
|
elif kind == 'hex':
|
|
if type(item) is not str or not re.fullmatch('[0-9a-f]{%d}' % rule['length'], item):
|
|
raise ValueError('hex')
|
|
if rule['nonzero'] and int(item, 16) == 0:
|
|
raise ValueError('zero identifier')
|
|
else:
|
|
raise ValueError('unknown grammar')
|
|
check(grammar['records'][record], value)
|
|
if record == 'inspect_response' and 'os_update' in value:
|
|
validate_os_status(value['os_update'])
|
|
if record == 'submit_request':
|
|
routes = {'UPDATER': ('APPLY', 'APPLY_REQUEST'),
|
|
'AUTOMATIC': ('RECOVER', 'AUTOMATIC_FAILURE'),
|
|
'MANUAL': ('RECOVER', 'MANUAL_REQUEST'),
|
|
'GUARD': ('RECOVER', 'CRASH_NOTICE'),
|
|
'BOOT': ('RECOVER', 'BOOT_RECONCILE'),
|
|
'OBSERVER': ('ACK_OBSERVATION', 'OBSERVATION_READY')}
|
|
if routes[value['route']] != (value['operation'], value['reason']):
|
|
raise ValueError('route binding')
|
|
if (value['requester_invocation_id'] is None) != (value['route'] in ('BOOT', 'OBSERVER')):
|
|
raise ValueError('invocation nullability')
|
|
if (value['observer_receipt_digest'] is not None) != (value['operation'] == 'ACK_OBSERVATION'):
|
|
raise ValueError('observer nullability')
|
|
if (value['selected_release_digest'] is not None) != (value['operation'] == 'APPLY'):
|
|
raise ValueError('selected release nullability')
|
|
if (value['staging_generation'] is not None) != (value['operation'] == 'APPLY'):
|
|
raise ValueError('staging generation nullability')
|
|
if record == 'receipt':
|
|
if (value['previous_receipt_digest'] is None) != (value['sequence'] == 1):
|
|
raise ValueError('predecessor')
|
|
if value['outcome'] == 'COMPLETE':
|
|
if value['phase'] not in ('COMPLETE', 'CLEANUP_INTENT', 'RETAINED'):
|
|
raise ValueError('completion phase')
|
|
for key in ('executor', 'observed_invocation_id', 'namespace_observation_digest',
|
|
'preservation_observation_digest', 'effective_config_observation_digest',
|
|
'config_pre_digest', 'config_post_digest', 'config_current_digest'):
|
|
if value[key] is None:
|
|
raise ValueError('incomplete observation')
|
|
return value
|
|
|
|
|
|
def validate_os_status(status):
|
|
confirmed = status['phase'] == 'POSTBOOT_CONFIRMED'
|
|
if status['post_boot_verified'] != confirmed:
|
|
raise ValueError('OS postboot disposition')
|
|
if confirmed and (status['reboot_required'] is not False or not status['installed_kernel']
|
|
or status['installed_kernel'] != status['running_kernel']):
|
|
raise ValueError('OS kernel not confirmed')
|
|
if status['phase'] == 'REBOOT_REQUIRED' and status['reboot_required'] is not True:
|
|
raise ValueError('OS reboot disposition')
|
|
if status['phase'] not in ('NO_OS_TRANSACTION', 'UNKNOWN') and (not status['supported'] or status['plan_sha256'] is None):
|
|
raise ValueError('OS plan/capability missing')
|
|
return status
|
|
|
|
|
|
def decode(record, raw):
|
|
return validate(record, parse(raw))
|
|
|
|
|
|
def bind_submit(request, response):
|
|
"""Cross-bind a response received on the live, pinned authenticated channel.
|
|
|
|
Only the owner can authenticate the protected acceptance and current head;
|
|
these checks are in addition to, never a substitute for, peer authentication.
|
|
"""
|
|
validate('submit_response', response)
|
|
hashed = digest(canonical(request))
|
|
if response['status'] == 'REFUSED':
|
|
if response['outcome'] != 'REFUSED' or any(response[k] is not None for k in
|
|
('acceptance', 'receipt', 'receipt_digest')):
|
|
raise ValueError('refusal shape')
|
|
return response
|
|
a, r = response['acceptance'], response['receipt']
|
|
if not a or not r or response['reservation'] != 'RETAINED':
|
|
raise ValueError('missing authenticated acceptance/head')
|
|
validate('receipt', r)
|
|
if response['request_digest'] != hashed or a['request_digest'] != hashed:
|
|
raise ValueError('request digest')
|
|
for key in ('operation', 'request_id', 'transaction_id', 'target_binding_digest',
|
|
'composition_digest', 'grant_digest'):
|
|
if a[key] != request[key]:
|
|
raise ValueError('acceptance binding')
|
|
for key in ('transaction_id', 'target_binding_digest', 'composition_digest'):
|
|
if a[key] != r[key]:
|
|
raise ValueError('head binding')
|
|
if a['accepted_sequence'] > r['sequence']:
|
|
raise ValueError('head precedes acceptance')
|
|
if a['accepted_sequence'] == r['sequence'] and any(a[k] != r[k] for k in ('operation', 'request_digest')):
|
|
raise ValueError('equal sequence identity')
|
|
if response['receipt_digest'] != digest(canonical(r)) or response['outcome'] != r['outcome']:
|
|
raise ValueError('receipt binding')
|
|
return response
|
|
|
|
|
|
def bind_inspect(request, response):
|
|
validate('inspect_response', response)
|
|
for key in ('challenge', 'target_binding_digest'):
|
|
if request[key] != response[key]:
|
|
raise ValueError('inspect challenge/target')
|
|
r = response['receipt']
|
|
if response['status'] == 'TRANSACTION':
|
|
if not r:
|
|
raise ValueError('missing head')
|
|
validate('receipt', r)
|
|
if request['transaction_id'] not in (None, r['transaction_id']):
|
|
raise ValueError('inspect transaction')
|
|
for key in ('transaction_id', 'target_binding_digest', 'admission_epoch_id'):
|
|
if response[key] != r[key]:
|
|
raise ValueError('inspect identity')
|
|
if response['head_sequence'] != r['sequence'] or response['receipt_digest'] != digest(canonical(r)) or response['head_digest'] != response['receipt_digest']:
|
|
raise ValueError('inspect head')
|
|
elif any(response[k] is not None for k in ('receipt', 'receipt_digest')):
|
|
raise ValueError('absent receipt')
|
|
if response['status'] == 'UNKNOWN' and any(response[k] is not None for k in ('head_digest', 'head_sequence', 'admission_epoch_id')):
|
|
raise ValueError('unknown continuity')
|
|
return response
|