Ë
    ÛJêjX4  ã                  óN  — U d Z ddlmZ ddlZddlZddlZddlZddlmZ ddl	m
Z
 ddlmZmZ dZded	<   d
Zd d„Z e«       Zded<    eh d£«      Zded<    G d„ dee
«      Z ed¬«       G d„ d«      «       Zd!d„Zd"d„Zd#d„Zd$d„Zd%d„Zd&d„Z	 	 	 	 	 	 	 	 	 	 d'd„Zd(d„Zg d¢Z y))u  Hash-chain primitives for the audit log â€” pure, stdlib-only, side-effect-free.

The chain links every audit record to its predecessor by digest, so an in-place
edit, a deletion, a reordering, or a truncation of the *beginning* severs the
sequence at a point a verifier can localise. This module builds the links and
verifies them; it never touches storage.

**What a keyless chain does NOT do â€” read before claiming anything about it.**
The digest is plain SHA-256 with no secret. Anyone who can read this file can
recompute every link, so an adversary holding write access to the database can
rewrite the log end-to-end and present a chain that verifies perfectly. The same
applies to deleting the *end* of the chain: the remaining records are a shorter
but perfectly valid chain. This chain is **tamper-evident, not tamper-proof**.
It detects corruption and naive editing; it does not detect a determined
attacker who recomputes. Closing that gap needs a keyed integrity mechanism or an
external anchor the writer cannot rewrite â€” deliberately out of scope here, and
recorded as an open item in the plan's F6 section.

These functions are intentionally free of any dependency on :class:`AuditLog`, so
that verification never inherits a bug from the code that wrote the chain.
é    )ÚannotationsN)Ú	dataclass)ÚEnum)ÚAnyÚFinalÚ@0000000000000000000000000000000000000000000000000000000000000000z
Final[str]ÚGENESISÚCYBERBRAIN_CHAIN_KEYc                 ó’   — t         j                  j                  t        «      } | st	        t        › d�«      ‚| j                  d«      S )uH   NaÄ�te HMAC klÃ­Ä� z env promÄ›nnÃ©; selÅ¾e, pokud chybÃ­ (fail-closed).zW environment variable is not set. Refusing to start with a predictable audit-chain key.úutf-8)ÚosÚenvironÚgetÚ_CHAIN_KEY_ENVÚRuntimeErrorÚencode)Úkeys    úM/home/oem/CyberBrain26-unified/src/cyberbrain26/security_plane/audit/chain.pyÚ_load_chain_keyr   (   sG   € ä
�*‰*�.‰.œÓ
(€CÙÜÜÐð Dð Dó
ð 	
ð �:‰:�gÓÐó    zFinal[bytes]Ú	CHAIN_KEY>   ÚseqÚ	prev_hashÚ
entry_hashzFinal[frozenset[str]]Ú_CHAIN_FIELDSc                  ó(   — e Zd ZdZdZdZdZdZdZdZ	y)	ÚTamperClassa  How a broken link most likely arose.

    Kept separate from "invalid" because the distinction is what makes a report
    actionable: a deletion and an edit are different incidents with different
    causes, and collapsing them into one boolean hides which one happened.
    ÚnoneÚin_place_editÚdeletionÚ
reorderingÚprefix_truncationÚmissing_linkN)
Ú__name__Ú
__module__Ú__qualname__Ú__doc__ÚNONEÚIN_PLACE_EDITÚDELETIONÚ
REORDERINGÚPREFIX_TRUNCATIONÚMISSING_LINK© r   r   r   r   <   s(   „ ñð €DØ#€MØ€HØ€JØ+ÐØ!�Lr   r   T)Úfrozenc                  ó�   — e Zd ZU dZded<   ded<   ded<   ded<   ded	<   d
Zded<   ej                  Zded<   dZ	ded<   dZ
ded<   y
)ÚVerificationReportah  Result of walking a chain. Frozen: the verdict is itself evidence.

    ``linkage_ok`` and ``integrity_ok`` are reported separately and must stay so.
    A chain can link perfectly while a single field inside one record was edited
    (``linkage_ok=True``, ``integrity_ok=False``); collapsing them into one
    boolean hides exactly which attack occurred.
    ÚboolÚintactÚ
linkage_okÚintegrity_okÚintÚrecords_checkedÚstrÚheadNz
int | NoneÚfirst_broken_seqr   Útamper_classÚ ÚdetailFÚunverified_premise)r$   r%   r&   r'   Ú__annotations__r:   r   r(   r;   r=   r>   r.   r   r   r1   r1   L   sW   … ñð ƒLØÓØÓØÓØ
ƒIØ#'Ð�jÓ'Ø +× 0Ñ 0€L�+Ó0Ø€FˆCÓð
  %Ð˜Ô$r   r1   c                ób  — t        | t        «      r| S t        | t        «      r| j                  «       rt	        | «      S t        | t
        «      r-| j                  «       D ��ci c]  \  }}|t        |«      “Œ c}}S t        | t        «      r| D �cg c]  }t        |«      ‘Œ c}S | S c c}}w c c}w )a   Canonicalise a value so hashing survives a storage round-trip.

    SQLite is typed and does not preserve the distinction between an integer and
    an integral float: ``latency_ms = 5.0`` can read back as ``5``. Hashing the
    raw value would therefore produce a false mismatch on an untouched record.
    Collapsing integral floats to ``int`` on both the write and the verify side
    makes the two agree regardless of how the value was stored. ``bool`` is a
    subclass of ``int`` and must be excluded, or ``True`` would become ``1``.
    )	Ú
isinstancer2   ÚfloatÚ
is_integerr6   ÚdictÚitemsÚ
_normaliseÚlist)ÚvalueÚkÚvs      r   rF   rF   e   sŒ   € ô �%œÔØˆÜ�%œÔ E×$4Ñ$4Ô$6Ü�5‹zÐÜ�%œÔØ-2¯[©[«]×;¡T Q¨�”:˜a“=Ñ Ó;Ð;Ü�%œÔØ',Ö- !”
˜1•Ò-Ð-Ø€Lùó <ùâ-s   Á!B&ÂB,c                óÒ   — | j                  «       D ��ci c]  \  }}|t        vsŒ|t        |«      “Œ }}}t        j                  |dddt
        ¬«      j                  d«      S c c}}w )u†  Deterministic serialisation of a record, for hashing.

    Sorted keys and fixed separators make key order and whitespace irrelevant, so
    a verifier can reproduce this from the persisted row alone. The hashed set is
    the whole record minus the chain columns â€” see :data:`_CHAIN_FIELDS`.

    Float formatting is a known fragility: a library upgrade that changes how a
    float renders would make every record fail re-verification even though
    nothing was tampered with. :func:`_normalise` removes the common case and
    ``tests/test_audit_chain.py`` pins the behaviour with a golden vector so a
    change cannot pass unnoticed.
    T)ú,ú:F)Ú	sort_keysÚ
separatorsÚensure_asciiÚdefaultr   )rE   r   rF   ÚjsonÚdumpsr8   r   )ÚrecordrI   rJ   Úpayloads       r   Úcanonical_event_bytesrV   z   s]   € ð -3¯L©L«N×U¡D A q¸aÄ}Ò>Tˆq”*˜Q“-ÑÐU€GÑUÜ�:‰:Ø˜4¨JÀUÔTWôç�fˆWƒoðùó Vs
   ”A#¥A#c                óp   — t        j                  | |j                  d«      z   «      j                  «       }|S )a  Return the hex digest binding ``canonical`` to ``prev_hash``.

    Deliberately ordered ``canonical || prev_hash``: the predecessor digest is the
    last thing folded in, which is what makes the link commit transitively to the
    entire prefix of the chain.
    Úascii)ÚhashlibÚsha256r   Ú	hexdigest)Ú	canonicalr   Údigests      r   Úcompute_linkr^   �   s0   € ô �^‰^˜I¨	×(8Ñ(8¸Ó(AÑAÓB×LÑLÓN€FØ€Mr   c                ó,   — t        t        | «      |«      S )zCConvenience wrapper: canonicalise ``record`` then compute its link.)r^   rV   )rT   r   s     r   Úlink_for_recordr`   ˜   s   € äÔ-¨fÓ5°yÓAÐAr   c                ó®   — t        | «      }t        j                  t        ||j	                  d«      z   t
        j                  «      j                  «       }|S )a  Return the HMAC-SHA256 digest binding ``event`` to ``previous_hash``.

    Uses HMAC rather than plain SHA-256 so that an attacker who can read the
    code but not the key cannot forge a valid chain. The key is embedded in this
    module; a production deployment would replace it with a secret from a key
    management system.

    The digest is computed over the canonical event bytes followed by the
    previous hash, creating a keyed link that is part of the hash-chain but
    protected by the secret key.
    rX   )rV   ÚhmacÚnewr   r   rY   rZ   r[   )ÚeventÚprevious_hashr\   r]   s       r   Úcompute_event_hashrf   �   sL   € ô & eÓ,€IÜ�X‰XÜØ�M×(Ñ(¨Ó1Ñ1Ü�‰ó÷ �iƒkð	 ð
 €Mr   c                ó
  — | st        ddddt        d¬«      S | d   j                  d«      }| d   j                  d«      st        ddddt        dd|› d	�¬
«      S t        }d}t        | «      D ]ü  \  }}|j                  d«      }|€&t        ddd||t        j
                  d|› d�¬«      c S |�2||dz   k7  r*t        ddd|||t        | |||«      d|dz   › d|› �¬«      c S |}|j                  d«      }|j                  d«      }||k7  r$t        ddd|||t        | ||«      d|› d�¬«      c S t        ||«      }	||	k7  r*t        ddd|dz   ||t        j                  d|› d�¬«      c S |	}Œþ t        dddt        | «      |¬«      S )uú  Walk ``records`` from genesis and report integrity, stopping at the first break.

    ``records`` must be ordered by ``seq`` ascending. Verification stops at the
    first break deliberately: once the chain diverges, every later record inherits
    an untrustworthy predecessor, so any mismatch after it is a consequence rather
    than an independent fact. Reporting only the first gives one defensible
    statement about where history diverged.

    Two invariants are checked separately at every step:

    * **integrity** â€” the record's stored digest equals the digest recomputed from
      its own stored fields. Catches an edit to any hashed field.
    * **linkage** â€” the record's stored ``prev_hash`` equals the running digest.
      Catches a deletion, a reorder, or an inserted record.

    A log whose first record carries no digest is reported as
    ``unverified_premise=True`` and ``intact=False``. Treating "never chained" as
    a pass is the failure mode where a green result proves nothing.
    Tr   z	empty log)r3   r4   r5   r7   r9   r=   r   r   Fz/log is not hash-chained: the first record (seq=zŠ) has no entry_hash. Rows written before the chain existed are not covered by it, so this is a declared boundary, not a clean verification)r3   r4   r5   r7   r9   r>   r=   Nzrecord z) has no seq, so its position is undefined)r3   r4   r5   r7   r9   r;   r=   é   zseq gap: expected z, found )r3   r4   r5   r7   r9   r:   r;   r=   r   zlink break at seq zF: prev_hash does not match the digest produced by the preceding recordzintegrity break at seq zV: stored entry_hash does not match the digest recomputed from this record's own fields)r3   r4   r5   r7   r9   )r1   r	   r   Ú	enumerater   r-   Ú_classify_gapÚ_classify_breakrf   r)   Úlen)
ÚrecordsÚ	first_seqÚrunningÚprevious_seqÚindexrT   r   Ústored_prevÚ
stored_ownÚ
recomputeds
             r   Úverify_chainru   ²   s$  € ñ( Ü!ØØØØÜØô
ð 	
ð ˜‘
—‘˜uÓ%€IØ�1‰:�>‰>˜,Ô'Ü!ØØØØÜØ#àAÀ)Àð MOð Oô
ð 	
ô €GØ#€Lä" 7Ó+ò ?‰ˆˆvØ�j‰j˜ÓˆØˆ;Ü%ØØ Ø"Ø %ØÜ(×5Ñ5Ø   Ð'PÐQôò ð Ð#¨¨|¸aÑ/?Ò(?Ü%ØØ Ø"Ø %ØØ!$Ü*¨7°E¸7ÀLÓQØ+¨L¸1Ñ,<Ð+=¸XÀcÀUÐKô	ò 	ð ˆà—j‘j Ó-ˆØ—Z‘Z Ó-ˆ
à˜'Ò!Ü%ØØ Ø"Ø %ØØ!$Ü,¨W°e¸WÓEà(¨¨ð .7ð 7ôò ô (¨°Ó8ˆ
Ø˜Ò#Ü%ØàØ"Ø %¨¡	ØØ!$Ü(×6Ñ6à-¨c¨Uð 3Fð Fôò ð ‰ð?ôB ØØØÜ˜G›Øôð r   c                óì   — | |d D �ch c]  }|j                  d«      ’Œ }}|dk(  rt        j                  S | |   j                  d«      |vr||vrt        j                  S t        j                  S c c}w )zKDecide whether a sequence gap means a deletion, a reorder, or a truncation.Nr   r   r   )r   r   r,   r*   r+   )rm   rq   ro   rp   ÚrÚlaters         r   rj   rj   -  sv   € ð +2°%°&¨/Ö: QˆQ�U‰U�<Õ Ð:€EÐ:Ø�‚zÜ×,Ñ,Ð,ð ˆu�~×Ñ˜+Ó&¨eÑ3¸ÀuÑ8LÜ×#Ñ#Ð#Ü×!Ñ!Ð!ùò ;s   ˆA1c                ó  — |dk(  rt         j                  S | D �ch c]  }|j                  d«      ’Œ c}t        hz  }| |   j                  d«      }||vrt         j                  S ||k7  rt         j
                  S t         j                  S c c}w )z6Classify a linkage break using the pointers around it.r   r   r   )r   r,   r   r	   r*   r+   r)   )rm   rq   ro   rw   ÚproducedÚpointers         r   rk   rk   >  s„   € à�‚zô ×,Ñ,Ð,à-4Ö5¨�—‘�lÕ#Ò5¼¸	ÑA€HØ�e‰n× Ñ  Ó-€GØ�hÑÜ×#Ñ#Ð#Ø�'ÒÜ×%Ñ%Ð%Ü×$Ñ$Ð$ùò 6s   šB
)	r   r	   r   r1   rV   rf   r^   r`   ru   )ÚreturnÚbytes)rH   r   r|   r   )rT   údict[str, Any]r|   r}   )r\   r}   r   r8   r|   r8   )rT   r~   r   r8   r|   r8   )rd   r~   re   r8   r|   r8   )rm   úlist[dict[str, Any]]r|   r1   )
rm   r   rq   r6   ro   r8   rp   r6   r|   r   )rm   r   rq   r6   ro   r8   r|   r   )!r'   Ú
__future__r   rY   rb   rR   r   Údataclassesr   Úenumr   Útypingr   r   r	   r?   r   r   r   Ú	frozensetr   r8   r   r1   rF   rV   r^   r`   rf   ru   rj   rk   Ú__all__r.   r   r   ú<module>r†      sè   ðòõ, #ã Û Û Û 	Ý !Ý ß ð €ˆÓ à'€óñ *Ó+€	ˆ<Ó +ñ (1Ò1SÓ'T€Ð$Ó Tô"�#�tô "ñ  �$Ô÷%ð %ó ð%ó0ó*ó&óBó
ó*xðv"Ø!ð"àð"ð ð"ð ð	"ð
 ó"ó"%ò 
�r   