use std::path::PathBuf;
use clap::Args;
use cortex_core::{Attestor, InMemoryAttestor};
use cortex_ledger::audit::{verify_signed_chain, FailureReason};
use ed25519_dalek::VerifyingKey;
use serde::Serialize;
use crate::exit::Exit;
use crate::output::{self, Envelope};
use crate::paths::DataLayout;
#[derive(Debug, Args)]
pub struct FromStoreArgs {
#[arg(long = "from-store")]
pub from_store: bool,
#[arg(long, value_name = "PATH", requires = "from_store")]
pub db: Option<PathBuf>,
#[arg(long, value_name = "PATH", requires = "from_store")]
pub event_log: Option<PathBuf>,
#[arg(
long,
value_name = "PATH",
requires = "from_store",
conflicts_with = "attestation"
)]
pub verification_key: Option<PathBuf>,
#[arg(
long,
value_name = "PATH",
requires = "from_store",
conflicts_with = "verification_key"
)]
pub attestation: Option<PathBuf>,
}
#[derive(Debug, Clone, Serialize)]
#[serde(tag = "state", rename_all = "snake_case")]
pub enum WitnessAxis {
Present {
axis: String,
detail: String,
payload: serde_json::Value,
},
Missing {
axis: String,
invariant: String,
detail: String,
},
}
impl WitnessAxis {
#[must_use]
#[allow(dead_code)]
pub fn axis(&self) -> &str {
match self {
Self::Present { axis, .. } | Self::Missing { axis, .. } => axis,
}
}
#[must_use]
pub const fn is_present(&self) -> bool {
matches!(self, Self::Present { .. })
}
}
#[derive(Debug, Clone, Serialize)]
pub struct FromStoreAssembly {
pub signed_chain: WitnessAxis,
pub ado_build: WitnessAxis,
pub rekor: WitnessAxis,
pub ots: WitnessAxis,
pub all_present: bool,
pub data_dir: String,
}
impl FromStoreAssembly {
#[must_use]
pub fn report(&self) -> serde_json::Value {
serde_json::json!({
"signed_chain": self.signed_chain,
"ado_build": self.ado_build,
"rekor": self.rekor,
"ots": self.ots,
"all_witness_axes_present": self.all_present,
"data_dir": self.data_dir,
})
}
}
pub fn release_witness_axis_missing(axis: &str) -> String {
format!("release.readiness.from_store.witness_axis_missing.{axis}")
}
pub fn compliance_witness_axis_missing(axis: &str) -> String {
format!("compliance.evidence.from_store.witness_axis_missing.{axis}")
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Surface {
Release,
Compliance,
}
impl Surface {
fn missing_invariant(self, axis: &str) -> String {
match self {
Self::Release => release_witness_axis_missing(axis),
Self::Compliance => compliance_witness_axis_missing(axis),
}
}
fn command_name(self) -> &'static str {
match self {
Self::Release => "release readiness",
Self::Compliance => "compliance evidence",
}
}
}
fn resolve_signed_chain_key(
args: &FromStoreArgs,
surface: Surface,
) -> Result<Option<(VerifyingKey, String)>, Exit> {
match (args.verification_key.as_ref(), args.attestation.as_ref()) {
(Some(path), None) => {
let bytes = std::fs::read(path).map_err(|err| {
eprintln!(
"cortex {}: cannot read --verification-key file `{}`: {err}",
surface.command_name(),
path.display()
);
Exit::PreconditionUnmet
})?;
let key_bytes: [u8; 32] = match bytes.as_slice().try_into() {
Ok(b) => b,
Err(_) => {
eprintln!(
"cortex {}: --verification-key file `{}` must be exactly 32 raw bytes (Ed25519 public key); got {} bytes",
surface.command_name(),
path.display(),
bytes.len()
);
return Err(Exit::PreconditionUnmet);
}
};
let key = VerifyingKey::from_bytes(&key_bytes).map_err(|err| {
eprintln!(
"cortex {}: --verification-key file `{}` is not a valid Ed25519 public key: {err}",
surface.command_name(),
path.display()
);
Exit::PreconditionUnmet
})?;
Ok(Some((key, hex_lower(&key_bytes))))
}
(None, Some(path)) => {
let bytes = std::fs::read(path).map_err(|err| {
eprintln!(
"cortex {}: cannot read --attestation key file `{}`: {err}",
surface.command_name(),
path.display()
);
Exit::PreconditionUnmet
})?;
if bytes.len() != 32 {
eprintln!(
"cortex {}: --attestation key file `{}` must be exactly 32 raw bytes (Ed25519 seed); got {} bytes",
surface.command_name(),
path.display(),
bytes.len()
);
return Err(Exit::PreconditionUnmet);
}
let mut seed = [0u8; 32];
seed.copy_from_slice(&bytes);
let attestor = InMemoryAttestor::from_seed(&seed);
let key = attestor.verifying_key();
Ok(Some((key, attestor.key_id().to_string())))
}
(None, None) => Ok(None),
(Some(_), Some(_)) => unreachable!("clap conflicts_with prevents both"),
}
}
pub fn assemble(args: &FromStoreArgs, surface: Surface) -> Result<FromStoreAssembly, Exit> {
let layout = DataLayout::resolve(args.db.clone(), args.event_log.clone())?;
let data_dir = layout.data_dir.display().to_string();
let signed_chain = assemble_signed_chain_axis(args, &layout, surface)?;
let ado_build = assemble_ado_build_axis(surface);
let rekor = assemble_rekor_axis(surface);
let ots = assemble_ots_axis(surface);
let all_present = signed_chain.is_present()
&& ado_build.is_present()
&& rekor.is_present()
&& ots.is_present();
Ok(FromStoreAssembly {
signed_chain,
ado_build,
rekor,
ots,
all_present,
data_dir,
})
}
fn assemble_signed_chain_axis(
args: &FromStoreArgs,
layout: &DataLayout,
surface: Surface,
) -> Result<WitnessAxis, Exit> {
let axis_name = "signed_chain";
if !layout.event_log_path.exists() {
return Ok(WitnessAxis::Missing {
axis: axis_name.into(),
invariant: surface.missing_invariant(axis_name),
detail: format!(
"no JSONL event log at `{}`; cannot derive a signed chain head",
layout.event_log_path.display()
),
});
}
let key = match resolve_signed_chain_key(args, surface)? {
Some(key) => key,
None => {
return Ok(WitnessAxis::Missing {
axis: axis_name.into(),
invariant: surface.missing_invariant(axis_name),
detail: "no --verification-key or --attestation supplied; cannot verify the signed chain head".into(),
});
}
};
let (verifying_key, key_id) = key;
match verify_signed_chain(&layout.event_log_path, &verifying_key, &key_id) {
Ok(outcome) => {
if outcome.report.ok() {
let (chain_head_hash, event_count) = chain_head_from_report(&outcome.report);
Ok(WitnessAxis::Present {
axis: axis_name.into(),
detail: format!(
"signed chain verified end-to-end under key_id={key_id}, {event_count} rows"
),
payload: serde_json::json!({
"key_id": key_id,
"rows_scanned": outcome.report.rows_scanned,
"chain_head_hash": chain_head_hash,
"event_count": event_count,
}),
})
} else {
let detail = signed_chain_failure_detail(&outcome.report);
Ok(WitnessAxis::Missing {
axis: axis_name.into(),
invariant: surface.missing_invariant(axis_name),
detail,
})
}
}
Err(err) => Ok(WitnessAxis::Missing {
axis: axis_name.into(),
invariant: surface.missing_invariant(axis_name),
detail: format!(
"signed chain at `{}` could not be scanned: {err}",
layout.event_log_path.display()
),
}),
}
}
fn chain_head_from_report(report: &cortex_ledger::Report) -> (String, u64) {
let event_count = report.rows_scanned as u64;
(String::new(), event_count)
}
fn signed_chain_failure_detail(report: &cortex_ledger::Report) -> String {
let summarized: Vec<String> = report
.failures
.iter()
.take(3)
.map(|f| {
let kind = match &f.reason {
FailureReason::Decode { .. } => "decode",
FailureReason::UnknownEventSchemaVersion { .. } => "unknown_event_schema_version",
FailureReason::PostCutoverV2AuditDispatchUnsupported { .. } => {
"post_cutover_v2_audit_dispatch_unsupported"
}
FailureReason::Orphan { .. } => "orphan",
FailureReason::HashBreak { .. } => "hash_break",
FailureReason::OrdinalGap { .. } => "ordinal_gap",
FailureReason::MissingSignature => "missing_signature",
FailureReason::BadSignature { .. } => "bad_signature",
FailureReason::UnknownAttestationSchemaVersion { .. } => {
"unknown_attestation_schema_version"
}
FailureReason::RotationEnvelopeRejected { .. } => "rotation_envelope_rejected",
};
format!("line {}: {kind}", f.line)
})
.collect();
let suffix = if report.failures.len() > 3 {
format!(" (+{} more)", report.failures.len() - 3)
} else {
String::new()
};
format!(
"signed chain verification produced {} per-row failure(s): {}{}",
report.failures.len(),
summarized.join(", "),
suffix
)
}
fn assemble_ado_build_axis(surface: Surface) -> WitnessAxis {
let axis_name = "ado_build";
WitnessAxis::Missing {
axis: axis_name.into(),
invariant: surface.missing_invariant(axis_name),
detail: "no ADO build evidence path is wired at HEAD; \
readiness cannot read build_id + signed manifest digest from audit_records yet"
.into(),
}
}
fn assemble_rekor_axis(surface: Surface) -> WitnessAxis {
let axis_name = "rekor";
WitnessAxis::Missing {
axis: axis_name.into(),
invariant: surface.missing_invariant(axis_name),
detail: "no Rekor receipt → chain head mapping is wired at HEAD; \
readiness cannot derive a fresh Rekor inclusion receipt from the local store"
.into(),
}
}
fn assemble_ots_axis(surface: Surface) -> WitnessAxis {
let axis_name = "ots";
WitnessAxis::Missing {
axis: axis_name.into(),
invariant: surface.missing_invariant(axis_name),
detail: "no OTS receipt → chain head mapping is wired at HEAD; \
readiness cannot derive a fresh OpenTimestamps receipt from the local store"
.into(),
}
}
pub fn emit_report(
assembly: &FromStoreAssembly,
command: &'static str,
envelope_command: &'static str,
forbidden_uses: &[&str],
) -> Exit {
let trusted_artifact_emitted = assembly.all_present;
let missing_axes: Vec<&str> = [
&assembly.signed_chain,
&assembly.ado_build,
&assembly.rekor,
&assembly.ots,
]
.iter()
.filter_map(|axis| match axis {
WitnessAxis::Missing { axis, .. } => Some(axis.as_str()),
WitnessAxis::Present { .. } => None,
})
.collect();
let report = serde_json::json!({
"command": command,
"mode": "from_store",
"artifact_emitted": trusted_artifact_emitted,
"trusted_artifact_emitted": trusted_artifact_emitted,
"trusted_stdout_artifact": trusted_artifact_emitted,
"independent_verification": trusted_artifact_emitted,
"forbidden_uses": forbidden_uses,
"evidence_input": assembly.report(),
"missing_witness_axes": missing_axes,
"all_witness_axes_present": trusted_artifact_emitted,
});
let exit = if trusted_artifact_emitted {
Exit::Ok
} else {
Exit::PreconditionUnmet
};
if output::json_enabled() {
let envelope = Envelope::new(envelope_command, exit, report);
return output::emit(&envelope, exit);
}
match serde_json::to_string_pretty(&report) {
Ok(serialized) => eprintln!("{serialized}"),
Err(err) => {
eprintln!("cortex {command}: failed to serialize --from-store report: {err}");
return Exit::Internal;
}
}
exit
}
fn hex_lower(bytes: &[u8]) -> String {
const HEX: &[u8; 16] = b"0123456789abcdef";
let mut out = String::with_capacity(bytes.len() * 2);
for b in bytes {
out.push(HEX[(b >> 4) as usize] as char);
out.push(HEX[(b & 0x0f) as usize] as char);
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn release_invariant_strings_are_stable() {
assert_eq!(
release_witness_axis_missing("signed_chain"),
"release.readiness.from_store.witness_axis_missing.signed_chain"
);
assert_eq!(
release_witness_axis_missing("ado_build"),
"release.readiness.from_store.witness_axis_missing.ado_build"
);
assert_eq!(
release_witness_axis_missing("rekor"),
"release.readiness.from_store.witness_axis_missing.rekor"
);
assert_eq!(
release_witness_axis_missing("ots"),
"release.readiness.from_store.witness_axis_missing.ots"
);
}
#[test]
fn compliance_invariant_strings_are_stable() {
assert_eq!(
compliance_witness_axis_missing("signed_chain"),
"compliance.evidence.from_store.witness_axis_missing.signed_chain"
);
assert_eq!(
compliance_witness_axis_missing("ado_build"),
"compliance.evidence.from_store.witness_axis_missing.ado_build"
);
assert_eq!(
compliance_witness_axis_missing("rekor"),
"compliance.evidence.from_store.witness_axis_missing.rekor"
);
assert_eq!(
compliance_witness_axis_missing("ots"),
"compliance.evidence.from_store.witness_axis_missing.ots"
);
}
#[test]
fn missing_axes_are_not_present() {
let axis = WitnessAxis::Missing {
axis: "signed_chain".into(),
invariant: release_witness_axis_missing("signed_chain"),
detail: "test".into(),
};
assert!(!axis.is_present());
assert_eq!(axis.axis(), "signed_chain");
}
#[test]
fn present_axis_carries_payload() {
let axis = WitnessAxis::Present {
axis: "signed_chain".into(),
detail: "ok".into(),
payload: serde_json::json!({ "key_id": "abc" }),
};
assert!(axis.is_present());
}
}