@@ -238,6 +238,98 @@ func computeEntryHash(store AuditStore, entry ChainEntry) (ChainEntry, error) {
238238 return entry , nil
239239}
240240
241+ // openChainVerify verifies the existing chain on New(). If the chain is corrupt
242+ // and AllowCorruptChainRecovery is false, it returns ErrCorruptChain.
243+ // If recovery is allowed, it writes a ChainSegmentBoundary entry and proceeds.
244+ func openChainVerify (store AuditStore , cfg Config ) error {
245+ entries , err := store .ReadAll ()
246+ if err != nil {
247+ return fmt .Errorf ("writ: read chain for verification: %w" , err )
248+ }
249+ if len (entries ) == 0 {
250+ return nil
251+ }
252+ verifyErr := verifyChain (entries )
253+ if verifyErr == nil {
254+ return nil
255+ }
256+ if ! cfg .AllowCorruptChainRecovery {
257+ return fmt .Errorf ("%w: %v" , ErrCorruptChain , verifyErr )
258+ }
259+ lastValid := findLastValidHash (entries )
260+ reason := fmt .Sprintf ("recovery at %s: %v" , time .Now ().UTC ().Format (time .RFC3339 ), verifyErr )
261+ boundary , err := buildSegmentBoundaryEntry (lastValid , reason )
262+ if err != nil {
263+ return fmt .Errorf ("writ: build recovery boundary: %w" , err )
264+ }
265+ return store .Append (boundary )
266+ }
267+
268+ // findLastValidHash walks entries and returns the hash of the last entry that
269+ // verifies correctly, or the genesis hash if none verify.
270+ func findLastValidHash (entries []ChainEntry ) string {
271+ prevHash := inaudit .PrevHashFor (nil )
272+ for _ , e := range entries {
273+ ie := inaudit.Entry {
274+ ID : e .ID ,
275+ PrevHash : e .PrevHash ,
276+ Hash : e .Hash ,
277+ EventType : e .EventType ,
278+ ActionType : e .ActionType ,
279+ Actor : e .Actor ,
280+ CallerID : e .CallerID ,
281+ InputHash : e .InputHash ,
282+ OutputHash : e .OutputHash ,
283+ Result : e .Result ,
284+ HookdTraceID : e .HookdTraceID ,
285+ Allowed : e .Allowed ,
286+ DenialReason : e .DenialReason ,
287+ Timestamp : timestampString (e .Timestamp ),
288+ }
289+ if ie .PrevHash != prevHash {
290+ break
291+ }
292+ want , err := inaudit .ComputeHash (ie )
293+ if err != nil || ie .Hash != want {
294+ break
295+ }
296+ prevHash = ie .Hash
297+ }
298+ return prevHash
299+ }
300+
301+ // buildSegmentBoundaryEntry creates a ChainSegmentBoundary entry that anchors
302+ // the start of a new chain segment after corruption recovery.
303+ func buildSegmentBoundaryEntry (lastValidHash , reason string ) (ChainEntry , error ) {
304+ id := newAuditID ()
305+ ts := time .Now ().UTC ()
306+
307+ internal := inaudit.Entry {
308+ ID : id ,
309+ PrevHash : lastValidHash ,
310+ EventType : "chain_segment_boundary" ,
311+ Allowed : true ,
312+ Timestamp : ts .Format (time .RFC3339Nano ),
313+ Metadata : map [string ]string {"type" : "RECOVERY" , "reason" : reason },
314+ }
315+
316+ hash , err := inaudit .ComputeHash (internal )
317+ if err != nil {
318+ return ChainEntry {}, fmt .Errorf ("compute boundary hash: %w" , err )
319+ }
320+ internal .Hash = hash
321+
322+ return ChainEntry {
323+ ID : internal .ID ,
324+ PrevHash : internal .PrevHash ,
325+ Hash : internal .Hash ,
326+ EventType : internal .EventType ,
327+ Allowed : internal .Allowed ,
328+ Timestamp : ts ,
329+ Metadata : internal .Metadata ,
330+ }, nil
331+ }
332+
241333// verifyChain is the internal entry point for Verify().
242334func verifyChain (entries []ChainEntry ) error {
243335 internalEntries := make ([]inaudit.Entry , len (entries ))
0 commit comments