pub enum ChainIdentity<'a> {
LegacyV1,
V2 {
caller_role: &'a str,
event_tip: &'a str,
},
V3 {
caller_role: &'a str,
event_tip: &'a str,
caller_principal: &'a str,
},
}Expand description
Immutable fields hashed into the chain. Status is intentionally absent — see module docs.
§Security contract — chain-content immutability
Every field in this struct is captured once at INSERT time and baked
into chain_hash = ed25519_sign(canonical(self) || prev_chain_hash, key).
After the row is written the hash is a one-time commitment: no field
in this struct may ever be mutated in place.
summary is the field most likely to attract a future “let me just fix
that typo” API. Do not add an update_summary (or similar) function.
If a correction is genuinely needed, choose one of the two safe options:
- Insert a corrective row — a new transaction row that references the
original
transaction_idin its ownsummary, leaving the original row and its chain hash untouched. - Extend the chain protocol — introduce a dedicated amendment record type with its own chain link, so that both the original commitment and the correction are auditable.
Any other approach silently breaks chain integrity: verify_chain will
flag the modified row as Broken because the stored signature will no
longer verify against the row’s content.
The canonical serialisation is stable across SQLite/Postgres backends. Each field is emitted as
<tag-name> 0x1E <tag-value> 0x1Fwhere 0x1E is the tag/value separator within a single field and 0x1F
is the field separator that terminates the field and introduces the
next one. We use the ASCII C0 byte values RS (0x1E) and US (0x1F)
because they are guaranteed not to appear in any normal text field, but
our role assignment is the inverse of the ASCII C0 convention
(where RS = “record separator” and US = “unit separator”). The names
are kept in the source for byte-level traceability against the canonical
buffer, not as a claim about ASCII semantics.
Inside a value, the four
bytes \\, NUL, RS, US are escaped to \\\\, \\0, \\1E, \\1F
respectively. The escape table is prefix-free (every escape starts
with \\), so any value can be injected without ambiguity. See
push_field for the implementation and tests for the round-trip.
Which generation of the canonical row encoding a row was signed under.
§Why this is an enum and not two Option fields
Adding a field to ChainContent changes the signed message, so every row
written before the change would re-encode differently and report as
Broken. Chains already exist in the field, and “delete your audit log to
upgrade” is not an acceptable migration for an audit log. The stored
chain_version column therefore selects the encoding per row: legacy
rows keep the exact bytes they were signed over, new rows carry the
identity fields.
A downgrade is not a hiding place. Rewriting a V2 row as LegacyV1 (to
erase caller_role) makes verification re-encode it without the identity
fields, so the stored signature — made over the V2 message — no longer
verifies and the row reports as Broken.
Variants§
LegacyV1
chain_version = 1. Written before the identity migration.
V2
chain_version = 2. Binds the row to the authenticated caller and to
the approval-event chain tip at insert time.
Fields
caller_role: &'a strRole the daemon resolved for the connection that requested this
action (SO_PEERCRED for Unix sockets, token for vsock). Signing it
lets an auditor answer which privilege tier asked; naming the account
is v3’s caller_principal.
event_tip: &'a strchain_hash of the last EventRow at insert time, or "" when
the event chain is empty. This is the cross-chain binding: because
checkpoints anchor the transaction chain tip, a committed
event_tip transitively anchors the event chain, so deleting
approval events below it becomes detectable.
V3
chain_version = 3. Everything v2 binds, plus the individual account.
v2 answers “an Admin did this”. On a host with two members of
sysknife-admin their signed records were indistinguishable, so the
trail could not answer the first question an investigation asks. v3
signs a principal as well.
Fields
caller_principal: &'a strScheme-prefixed identity of the caller, produced by
crate::auth::CallerPrincipal: uid:1000 for a Unix-socket peer
whose credentials the kernel attested, token:vsock for a vsock
connection authenticated by the pre-shared token, and
none:unattributed when the daemon could establish neither.
The scheme is signed along with the value on purpose. An auditor must be able to tell a kernel-attested account from a shared secret that any holder could have presented, and a bare string would erase that difference.
Implementations§
Trait Implementations§
Source§impl<'a> Clone for ChainIdentity<'a>
impl<'a> Clone for ChainIdentity<'a>
impl<'a> Copy for ChainIdentity<'a>
Source§impl<'a> Debug for ChainIdentity<'a>
impl<'a> Debug for ChainIdentity<'a>
impl<'a> Eq for ChainIdentity<'a>
Source§impl<'a> PartialEq for ChainIdentity<'a>
impl<'a> PartialEq for ChainIdentity<'a>
impl<'a> StructuralPartialEq for ChainIdentity<'a>
Auto Trait Implementations§
impl<'a> Freeze for ChainIdentity<'a>
impl<'a> RefUnwindSafe for ChainIdentity<'a>
impl<'a> Send for ChainIdentity<'a>
impl<'a> Sync for ChainIdentity<'a>
impl<'a> Unpin for ChainIdentity<'a>
impl<'a> UnsafeUnpin for ChainIdentity<'a>
impl<'a> UnwindSafe for ChainIdentity<'a>
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Source§impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> CloneToUninit for Twhere
T: Clone,
Source§impl<Q, K> Equivalent<K> for Q
impl<Q, K> Equivalent<K> for Q
Source§fn equivalent(&self, key: &K) -> bool
fn equivalent(&self, key: &K) -> bool
key and return true if they are equal.Source§impl<Q, K> Equivalent<K> for Q
impl<Q, K> Equivalent<K> for Q
Source§impl<T> Instrument for T
impl<T> Instrument for T
Source§fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
Source§fn in_current_span(self) -> Instrumented<Self> ⓘ
fn in_current_span(self) -> Instrumented<Self> ⓘ
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left is true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left(&self) returns true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read more