Skip to content

Commit fdf7804

Browse files
committed
ci: fix ci
1 parent 30d6012 commit fdf7804

3 files changed

Lines changed: 115 additions & 107 deletions

File tree

crate/clients/clap/src/actions/audit.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -592,7 +592,10 @@ mod tests {
592592
.run_with_writer(&mut out)
593593
.unwrap();
594594
let msg = String::from_utf8(out).unwrap();
595-
assert!(msg.contains("audit.jsonl: chain OK: 1 event verified"), "{msg}");
595+
assert!(
596+
msg.contains("audit.jsonl: chain OK: 1 event verified"),
597+
"{msg}"
598+
);
596599
assert!(
597600
!msg.contains(&sealed_path.display().to_string()),
598601
"sealed evidence must be digest-checked via its reanchor, not verified as a chain: {msg}"

crate/server/src/core/audit/file_store.rs

Lines changed: 104 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -90,10 +90,7 @@ impl AuditFileStore {
9090
/// Returns an error only if `channel_capacity` is 0 — a pure configuration mistake,
9191
/// not a runtime condition. Every other fault (I/O, lock contention, log corruption)
9292
/// is handled inside the writer task without aborting startup; see `writer_supervisor`.
93-
pub(crate) fn start(
94-
path: &Path,
95-
channel_capacity: usize,
96-
) -> KResult<Self> {
93+
pub(crate) fn start(path: &Path, channel_capacity: usize) -> KResult<Self> {
9794
if channel_capacity == 0 {
9895
return Err(KmsError::ServerError(
9996
"audit: channel_capacity must be at least 1".to_owned(),
@@ -234,20 +231,19 @@ enum TailOutcome {
234231
},
235232
}
236233

234+
/// Metadata for the first invalid interior row.
235+
struct InteriorChainFailure {
236+
reason: SealReason,
237+
claimed_last_id: Option<i64>,
238+
failure_offset: u64,
239+
}
240+
237241
/// Result of verifying every audit row except the physical tail row.
238242
///
239243
/// `previous_event` is the row the tail must link to when the tail is complete.
240-
enum InteriorChainOutcome {
241-
/// Every interior row is valid and linked; the physical tail remains for `classify_tail`.
242-
Valid {
243-
previous_event: Option<AuditEvent>,
244-
},
245-
/// An interior row failed verification or chain-link validation.
246-
Broken {
247-
reason: SealReason,
248-
claimed_last_id: Option<i64>,
249-
failure_offset: u64,
250-
},
244+
struct InteriorChainVerification {
245+
previous_event: Option<AuditEvent>,
246+
failure: Option<InteriorChainFailure>,
251247
}
252248

253249
/// Classifies the tail of `path` to decide how startup should recover.
@@ -378,12 +374,7 @@ fn classify_tail(path: &Path, previous_event: Option<&AuditEvent>) -> KResult<Ta
378374
needs_leading_nl: false,
379375
}
380376
}
381-
RowCheck::Verified(event) => TailOutcome::SealAndRoll {
382-
reason: SealReason::HashMismatch,
383-
claimed_last_id: Some(event.id),
384-
failure_offset: last_start,
385-
},
386-
RowCheck::HashMismatch(event) => TailOutcome::SealAndRoll {
377+
RowCheck::Verified(event) | RowCheck::HashMismatch(event) => TailOutcome::SealAndRoll {
387378
reason: SealReason::HashMismatch,
388379
claimed_last_id: Some(event.id),
389380
failure_offset: last_start,
@@ -718,10 +709,11 @@ fn try_acquire_lock(lock_path: &Path) -> std::io::Result<std::fs::File> {
718709
///
719710
/// # Errors
720711
/// Returns an error only if the file cannot be opened or read.
721-
fn verify_interior_chain(path: &Path) -> KResult<InteriorChainOutcome> {
712+
fn verify_interior_chain(path: &Path) -> KResult<InteriorChainVerification> {
722713
if !path.exists() {
723-
return Ok(InteriorChainOutcome::Valid {
714+
return Ok(InteriorChainVerification {
724715
previous_event: None,
716+
failure: None,
725717
});
726718
}
727719

@@ -756,25 +748,34 @@ fn verify_interior_chain(path: &Path) -> KResult<InteriorChainOutcome> {
756748
if verify_chain_link(&event, prev.as_ref()) {
757749
prev = Some(event);
758750
} else {
759-
return Ok(InteriorChainOutcome::Broken {
760-
reason: SealReason::HashMismatch,
761-
claimed_last_id: Some(event.id),
762-
failure_offset: pending_offset,
751+
return Ok(InteriorChainVerification {
752+
previous_event: None,
753+
failure: Some(InteriorChainFailure {
754+
reason: SealReason::HashMismatch,
755+
claimed_last_id: Some(event.id),
756+
failure_offset: pending_offset,
757+
}),
763758
});
764759
}
765760
}
766761
RowCheck::HashMismatch(event) => {
767-
return Ok(InteriorChainOutcome::Broken {
768-
reason: SealReason::HashMismatch,
769-
claimed_last_id: Some(event.id),
770-
failure_offset: pending_offset,
762+
return Ok(InteriorChainVerification {
763+
previous_event: None,
764+
failure: Some(InteriorChainFailure {
765+
reason: SealReason::HashMismatch,
766+
claimed_last_id: Some(event.id),
767+
failure_offset: pending_offset,
768+
}),
771769
});
772770
}
773771
RowCheck::Unparseable => {
774-
return Ok(InteriorChainOutcome::Broken {
775-
reason: SealReason::Unparseable,
776-
claimed_last_id: prev.as_ref().map(|p| p.id),
777-
failure_offset: pending_offset,
772+
return Ok(InteriorChainVerification {
773+
previous_event: None,
774+
failure: Some(InteriorChainFailure {
775+
reason: SealReason::Unparseable,
776+
claimed_last_id: prev.as_ref().map(|p| p.id),
777+
failure_offset: pending_offset,
778+
}),
778779
});
779780
}
780781
}
@@ -783,8 +784,9 @@ fn verify_interior_chain(path: &Path) -> KResult<InteriorChainOutcome> {
783784
pending = Some((line, line_offset));
784785
}
785786

786-
Ok(InteriorChainOutcome::Valid {
787+
Ok(InteriorChainVerification {
787788
previous_event: prev,
789+
failure: None,
788790
})
789791
}
790792

@@ -802,75 +804,73 @@ fn verify_interior_chain(path: &Path) -> KResult<InteriorChainOutcome> {
802804
/// Returns an error only for content-independent I/O faults (cannot read/truncate/rename/
803805
/// open); a data-corruption condition is always routed to a `TailOutcome` variant instead
804806
/// and handled without error (see `classify_tail`).
805-
fn recover_and_open(
806-
path: &Path,
807-
) -> KResult<(std::fs::File, i64, [u8; 32])> {
808-
let (next_id, prev_hash) = match verify_interior_chain(path)? {
809-
InteriorChainOutcome::Broken {
810-
reason,
811-
claimed_last_id,
812-
failure_offset,
813-
} => {
814-
seal_and_roll(path, reason, claimed_last_id, failure_offset)?
815-
}
816-
InteriorChainOutcome::Valid { previous_event } => {
817-
match classify_tail(path, previous_event.as_ref())? {
818-
TailOutcome::Genesis => (0, [0_u8; 32]),
819-
TailOutcome::Resume {
820-
next_id,
821-
prev_hash,
822-
needs_leading_nl,
823-
} => {
824-
if needs_leading_nl {
825-
// The prior process wrote the JSON row but crashed before its trailing
826-
// '\n' hit disk. The row itself is valid — just fix the line boundary
827-
// before the writer task appends anything new.
828-
let mut f = open_append(path).map_err(|e| {
829-
KmsError::ServerError(format!(
830-
"audit: cannot repair missing line terminator in {}: {e}",
831-
path.display()
832-
))
833-
})?;
834-
f.write_all(b"\n").map_err(|e| {
835-
KmsError::ServerError(format!(
836-
"audit: cannot repair missing line terminator in {}: {e}",
837-
path.display()
838-
))
839-
})?;
840-
f.sync_data().map_err(|e| {
841-
KmsError::ServerError(format!(
842-
"audit: cannot sync line-terminator repair in {}: {e}",
843-
path.display()
844-
))
845-
})?;
846-
} else {
847-
debug!(
848-
"AuditFileStore: resuming at id={next_id}, prev_hash={}",
849-
hex::encode(&prev_hash[..8]) // first 8 bytes (16 hex chars) sufficient for diagnostics
850-
);
851-
}
852-
(next_id, prev_hash)
807+
fn recover_and_open(path: &Path) -> KResult<(std::fs::File, i64, [u8; 32])> {
808+
let verification = verify_interior_chain(path)?;
809+
let (next_id, prev_hash) = if let Some(failure) = verification.failure {
810+
seal_and_roll(
811+
path,
812+
failure.reason,
813+
failure.claimed_last_id,
814+
failure.failure_offset,
815+
)?
816+
} else {
817+
let previous_event = verification.previous_event;
818+
match classify_tail(path, previous_event.as_ref())? {
819+
TailOutcome::Genesis => (0, [0_u8; 32]),
820+
TailOutcome::Resume {
821+
next_id,
822+
prev_hash,
823+
needs_leading_nl,
824+
} => {
825+
if needs_leading_nl {
826+
// The prior process wrote the JSON row but crashed before its trailing
827+
// '\n' hit disk. The row itself is valid — just fix the line boundary
828+
// before the writer task appends anything new.
829+
let mut f = open_append(path).map_err(|e| {
830+
KmsError::ServerError(format!(
831+
"audit: cannot repair missing line terminator in {}: {e}",
832+
path.display()
833+
))
834+
})?;
835+
f.write_all(b"\n").map_err(|e| {
836+
KmsError::ServerError(format!(
837+
"audit: cannot repair missing line terminator in {}: {e}",
838+
path.display()
839+
))
840+
})?;
841+
f.sync_data().map_err(|e| {
842+
KmsError::ServerError(format!(
843+
"audit: cannot sync line-terminator repair in {}: {e}",
844+
path.display()
845+
))
846+
})?;
847+
} else {
848+
debug!(
849+
"AuditFileStore: resuming at id={next_id}, prev_hash={}",
850+
hex::encode(&prev_hash[..8]) // first 8 bytes (16 hex chars) sufficient for diagnostics
851+
);
853852
}
854-
TailOutcome::TruncateContinue {
855-
keep_len,
856-
next_id,
857-
prev_hash,
858-
bytes_discarded,
859-
discard_offset,
860-
} => truncate_and_continue(
861-
path,
862-
keep_len,
863-
next_id,
864-
prev_hash,
865-
bytes_discarded,
866-
discard_offset,
867-
)?,
868-
TailOutcome::SealAndRoll {
869-
reason,
870-
claimed_last_id,
871-
failure_offset,
872-
} => seal_and_roll(path, reason, claimed_last_id, failure_offset)?,
853+
(next_id, prev_hash)
873854
}
855+
TailOutcome::TruncateContinue {
856+
keep_len,
857+
next_id,
858+
prev_hash,
859+
bytes_discarded,
860+
discard_offset,
861+
} => truncate_and_continue(
862+
path,
863+
keep_len,
864+
next_id,
865+
prev_hash,
866+
bytes_discarded,
867+
discard_offset,
868+
)?,
869+
TailOutcome::SealAndRoll {
870+
reason,
871+
claimed_last_id,
872+
failure_offset,
873+
} => seal_and_roll(path, reason, claimed_last_id, failure_offset)?,
874874
}
875875
};
876876

@@ -1125,9 +1125,7 @@ mod tests {
11251125
sync::{Arc, atomic::AtomicU64},
11261126
};
11271127

1128-
use cosmian_kms_access::audit::{
1129-
AuditEvent, AuditEventDraft, compute_row_hash, verify_event,
1130-
};
1128+
use cosmian_kms_access::audit::{AuditEvent, AuditEventDraft, compute_row_hash, verify_event};
11311129
use time::OffsetDateTime;
11321130
use tokio::sync::mpsc;
11331131

documentation/docs/configuration/log-reference.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -679,6 +679,13 @@ Crate path: `crate/server`
679679
| `trace` | `ModifyAttribute: Extractable: {:?}` | `src/core/operations/attributes/modify.rs` | - | - |
680680
| `trace` | `ModifyAttribute: Sensitive: {:?}` | `src/core/operations/attributes/modify.rs` | - | - |
681681
| `trace` | `Set Attribute: Sensitive: {:?}` | `src/core/operations/attributes/set.rs` | - | - |
682+
| `error` | `audit: event not queued — rejecting response (reject mode)` | `src/middlewares/audit.rs` | - | Audit failure in reject mode: single event failed to queue (channel full or closed). HTTP response is 503 ServiceUnavailable. |
683+
| `error` | `audit: event(s) not queued — rejecting response (reject mode)` | `src/middlewares/audit.rs` | - | Audit failure in reject mode: batch request (one or more events) failed to queue. HTTP response is 503 ServiceUnavailable. |
684+
| `error` | `AuditFileStore: audit log lock {} held by another instance ({e}) — buffering events until it is released` | `src/core/audit/file_store.rs` | `e`: lock acquisition error | Normal in HA deployments: another KMS instance holds the audit log lock. Events buffered (up to channel capacity) and file open will retry periodically. |
685+
| `error` | `AuditFileStore: cannot open audit log {} ({e}) — retrying` | `src/core/audit/file_store.rs` | `e`: file I/O error | File not readable (permissions, missing, corrupted header). Events buffered; retrying periodically. |
686+
| `error` | `AuditFileStore: sealed corrupted audit log as {} (reason={}, sha256={sha256_hex}, size={size}, claimed_last_id={claimed_last_id:?}, failure_offset={failure_offset}) — starting a fresh chain` | `src/core/audit/file_store.rs` | `sha256_hex`: SHA256 of sealed file<br>`size`: file size in bytes<br>`claimed_last_id`: last event ID if readable<br>`failure_offset`: byte offset where corruption detected | **Security:** Corrupted log sealed as forensic evidence with RFC3339 timestamp. New chain started at id=1. Offline verification: `ckms audit verify --path <sealed_file>` |
687+
| `error` | `AuditFileStore: torn write recovered — discarded {bytes_discarded} byte(s) at offset {discard_offset} (process likely killed mid-write); resuming chain at id={next_id}` | `src/core/audit/file_store.rs` | `bytes_discarded`: incomplete bytes dropped<br>`discard_offset`: offset in file<br>`next_id`: resuming event ID | Process killed mid-write (crash/SIGKILL). Incomplete event discarded; hash chain preserved. |
688+
| `debug` | `AuditFileStore: still waiting on audit log lock {} ({e})` | `src/core/audit/file_store.rs` | `e`: lock acquisition error | Debug: subsequent retry attempt (not the first). Implies a preceding "audit log lock held by another instance" error. |
682689

683690
### `cosmian_kms_server_database`
684691

0 commit comments

Comments
 (0)