#!/usr/bin/env python3
"""Offline verifier for the downloaded owned synthetic lab packet; no product source or private key."""
from __future__ import annotations
import hashlib,json,re,sys,ssl
from pathlib import Path
import oqs
from jsonschema import Draft7Validator,FormatChecker
from referencing import Registry,Resource
from referencing.jsonschema import DRAFT7
ROOT=Path(__file__).resolve().parent

def need(ok,name):
    if not ok:raise AssertionError(name)
def digest(data):return hashlib.sha256(data).hexdigest()
def load(name):return json.loads((ROOT/name).read_bytes())

def registry():
    pairs=[]
    for name in ['bom-1.7.schema.json','jsf-0.82.schema.json','spdx.schema.json','cryptography-defs.schema.json']:
        doc=load('schema/'+name);resource=Resource.from_contents(doc,default_specification=DRAFT7)
        pairs.append((name,resource))
        if doc.get('$id'):pairs.append((doc['$id'],resource))
    return Registry().with_resources(pairs)

def main():
    manifest=load('packet-manifest.json')
    need(manifest['source_git_sha']=='03e3b4ff300b6ca8c3b9a18f49a95428b2c8466f','source_identity')
    for rel,item in manifest['files'].items():
        need('..' not in Path(rel).parts and not rel.startswith('/'),'manifest_path')
        data=(ROOT/rel).read_bytes()
        need(len(data)==item['bytes'] and digest(data)==item['sha256'],'digest:'+rel)
    need(not any(p.name.startswith('ml-dsa-65-private') for p in ROOT.rglob('*')),'private_key_excluded')
    checker=FormatChecker()
    need(checker.conforms('2026-09-30T12:00:00Z','date-time') and not checker.conforms('2026-09-30T12:00:00+00:00Z','date-time'),'effective_datetime_format_checker')
    validator=Draft7Validator(load('schema/bom-1.7.schema.json'),registry=registry(),format_checker=checker)
    before=load('before-native-cbom.json');after=load('after-native-cbom.json');original=load('original-failing-before-cbom.json')
    need(not list(validator.iter_errors(before)),'before_schema')
    need(not list(validator.iter_errors(after)),'after_schema')
    errors=list(validator.iter_errors(original));need(bool(errors),'original_must_fail_schema')
    need(len(before.get('components',[]))==1 and before['components'][0]['name']=='MD5','before_component')
    need(len(after.get('components',[]))==0,'after_component')
    run=load('scan-cbom-run.json');bind=load('lab-binding-manifest.json');finding=load('before-native-findings.json');after_findings=load('after-native-findings.json')
    need(run['source_git_sha']==manifest['source_git_sha']==bind['source_git_sha'],'source_run_binding')
    need(run['passed_native_scan'] and run['passed_official_schema'],'run_result')
    need(run['before']['strict_format_errors']==[] and run['after']['strict_format_errors']==[],'format_aware_receipt')
    need(run['before']['counters']=={'files_scanned':1,'files_skipped':0,'parse_errors':0,'logged_errors':[]},'before_coverage')
    need(run['after']['counters']=={'files_scanned':1,'files_skipped':0,'parse_errors':0,'logged_errors':[]},'after_coverage')
    need(len(finding)==1 and finding[0]['raw_data']['pattern_name']=='md5_usage' and after_findings==[],'native_findings')
    fb=bind['finding_binding']
    need(fb['native_stable_id'] is None and fb['observed_target_path_historical']==finding[0]['target'],'historical_target')
    need(digest((ROOT/'archived-before-service.py').read_bytes())==fb['immutable_before_archive_sha256']==run['fixture_digests']['before_sha256'],'before_archive')
    need(digest((ROOT/'archived-after-service.py').read_bytes())==fb['immutable_after_archive_sha256']==run['fixture_digests']['after_sha256'],'after_archive')
    need(fb['final_mutable_target_state']=='after_fixture' and bind['final_mutable_target_sha256']==fb['immutable_after_archive_sha256'],'final_target_state')
    need(digest((ROOT/'before-native-cbom.json').read_bytes())==run['before']['cbom_sha256']==bind['before_native_cbom_sha256'],'before_cbom_binding')
    need(digest((ROOT/'after-native-cbom.json').read_bytes())==run['after']['cbom_sha256']==bind['after_native_cbom_sha256'],'after_cbom_binding')
    pub=(ROOT/'ml-dsa-65-public.key').read_bytes()
    with oqs.Signature('ML-DSA-65') as verifier:
        for name in ['before-native-cbom.json','after-native-cbom.json','lab-binding-manifest.json']:
            data=(ROOT/name).read_bytes();sig=(ROOT/(name+'.sig')).read_bytes()
            need(verifier.verify(data,sig,pub),'signature:'+name)
            bad=bytearray(data);bad[0]^=1
            need(not verifier.verify(bytes(bad),sig,pub),'one_byte_tamper_rejected:'+name)
    workflow=load('workflow-summary.json');cases=workflow['cases']
    need(workflow['http_event_count']==18,'loopback_http_event_count')
    need(cases['verified_closed']['closed_loop_ok'] and cases['verified_closed']['verdict']=='verified_closed','positive_closure')
    for name in ['open_ticket','exposure_remains','unverified_rescan','connector_failure']:
        need(not cases[name]['closed_loop_ok'] and cases[name]['verdict']!='verified_closed','negative_closure:'+name)
    trace=load('tls-trace-transition.json');original_tls=load('tls-original-transition.json');tls=load('tls-summary.json')
    need(digest((ROOT/'tls-trace-script.txt').read_bytes())==trace['script_sha256']==tls['trace_script_sha256'],'tls_trace_script_custody')
    need(digest((ROOT/'tls-original-script.txt').read_bytes())==original_tls['script_sha256'],'tls_original_script_custody')
    txt=(ROOT/'tls-trace.txt').read_text(errors='replace')
    need(digest((ROOT/'tls-trace.txt').read_bytes())==tls['trace_sha256'],'tls_trace_digest')
    for group in ['ecdh_x25519 (29)','X25519MLKEM768 (4588)','MLKEM768 (513)']:
        need('NamedGroup: '+group in txt,'named_group:'+group)
    phases={p['name']:p for p in trace['phases']}
    for name in ['classical_x25519','hybrid_x25519mlkem768','pure_mlkem768','rollback_x25519']:
        need(phases[name]['application_success'] and phases[name]['listener_closed'] and phases[name]['client_tls']['protocol']=='TLSv1.3','tls_success:'+name)
    incompat=phases['classical_only_client_against_hybrid']
    need(not incompat.get('application_success',False) and any(x['code']=='ERR_SSL_NO_SUITABLE_KEY_SHARE' for x in incompat['handshake_errors']),'tls_incompatibility')
    continuity=load('continuity.json')
    need(digest((ROOT/'continuity-script.txt').read_bytes())==continuity['script_sha256'],'continuity_script_custody')
    need(continuity['script_sha256']=='afb97d36ba46386e5e0b8896866813c0957bb6623182af26491de9f58586010a','continuity_presealed_script')
    cert=(ROOT/'continuity-cert.pem').read_text()
    cert_der=ssl.PEM_cert_to_DER_cert(cert)
    need(digest(cert_der).upper()==continuity['certificate_fingerprint'].replace(':','').upper(),'continuity_cert_fingerprint')
    probes=continuity['probes'];compatible=[p for p in probes if p['kind']=='compatible']
    good=[p for p in compatible if p['success']];bad=[p for p in compatible if not p['success']]
    need(continuity['same_port'] and continuity['port_released'] and len(continuity['phases'])==3,'continuity_port_custody')
    need(len(compatible)==32 and len(good)==26 and len(bad)==6,'continuity_probe_counts')
    need(all(p['authorization'] is True and p['response_status']=='200' and p['response_body']=='owned-lab-ok' for p in good),'continuity_authenticated_success')
    need(all(p['phase_at_start'].startswith('transition_') and p['error']['code']=='ECONNREFUSED' and p['response_status'] is None for p in bad),'continuity_measured_restart_failures')
    negatives={p['kind']:p for p in probes if p['kind']!='compatible'}
    need(negatives['classical_only_negative']['phase_at_start']=='hybrid' and not negatives['classical_only_negative']['success'] and 'HANDSHAKE_FAILURE' in negatives['classical_only_negative']['error']['code'] and negatives['classical_only_negative']['response_status'] is None,'continuity_classical_refusal')
    need(not negatives['wrong_ca_negative']['success'] and negatives['wrong_ca_negative']['error']['code']=='DEPTH_ZERO_SELF_SIGNED_CERT' and negatives['wrong_ca_negative']['response_status'] is None,'continuity_untrusted_ca_refusal')
    need(negatives['rollback_classical']['success'] and negatives['rollback_classical']['response_status']=='200','continuity_rollback')
    trace2=(ROOT/'continuity-trace.txt').read_text(errors='replace')
    need('NamedGroup: ecdh_x25519 (29)' in trace2 and 'NamedGroup: X25519MLKEM768 (4588)' in trace2,'continuity_trace_groups')
    need('restart' in (ROOT/'continuity-protocol.md').read_text().lower(),'continuity_protocol')
    print(json.dumps({'ok':True,'source_git_sha':manifest['source_git_sha'],'files_checked':len(manifest['files']),'strict_cbom_before_after':True,'original_schema_failed':True,'original_error_count':len(errors),'signatures_valid':3,'one_byte_tamper_rejected':3,'workflow_positive':1,'workflow_negative':4,'tls_successful_phases':4,'tls_classical_refusal':True,'continuity_compatible_success':26,'continuity_restart_failures':6},sort_keys=True))
if __name__=='__main__':
    try:main()
    except Exception as exc:
        print(json.dumps({'ok':False,'error':str(exc)},sort_keys=True),file=sys.stderr)
        sys.exit(1)
