Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changes/2011.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Report the hash that chain links use for every exported audit entry. `audit.export` gave a format-v2 entry's `content_hash_hex` as BLAKE3 of its signing data, which for v2 is the signature wrapper of the entry hash, so an exported chain that had switched to v2 did not link to its entries or to its head in `audit.heads`. v2 entries now export their SHA-256 entry hash; v1 entries are unchanged.
22 changes: 14 additions & 8 deletions crates/astrid-core/src/kernel_api/audit_export.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,13 +77,14 @@ pub struct AuditHeadsChain {
/// [`AUDIT_OMITTED_TOTAL_UNKNOWN`]. A known total never decreases, so
/// `omitted_total + count` counts every entry the chain has held.
pub omitted_total: u64,
/// Hex BLAKE3 content hash of the head entry (what the next entry's
/// `previous_hash` links to), or 64 zeros for an empty chain.
/// Hex content hash of the head entry (what the next entry's
/// `previous_hash` links to), or 64 zeros for an empty chain. See
/// [`AuditExportEntry::content_hash_hex`] for how it is computed.
pub head_hash_hex: String,
/// Head entry id, if the chain has entries.
pub head_id: Option<String>,
/// Stored RFC 3339 timestamp of the head entry. Entry signatures cover
/// whole seconds only.
/// Stored RFC 3339 timestamp of the head entry. A format-v1 entry
/// signature covers whole seconds only.
pub last_timestamp: Option<String>,
/// Latest prune receipt summary, if the chain was ever pruned. Not part
/// of the signed bytes.
Expand Down Expand Up @@ -247,18 +248,23 @@ pub struct AuditExportEntry {
pub index: u64,
/// Entry id (UUID).
pub id: String,
/// Stored RFC 3339 timestamp. The signature covers whole seconds only.
/// Stored RFC 3339 timestamp. A format-v1 signature covers whole
/// seconds only.
pub timestamp: String,
/// Hex hash of the previous entry; zeros for the genesis entry.
pub previous_hash_hex: String,
/// Hex `BLAKE3(signing_data)`, the value the next entry's
/// `previous_hash` links to.
/// Hex content hash, the value the next entry's `previous_hash` links
/// to: `BLAKE3(signing_data)` for a format-v1 entry, and for a format-v2
/// entry (one whose stored form has a `v2` field) the SHA-256 entry hash
/// of its canonical body.
pub content_hash_hex: String,
/// Hex Ed25519 signature over `signing_data`.
pub signature_hex: String,
/// Hex Ed25519 public key embedded in the entry.
pub public_key_hex: String,
/// Hex of the exact bytes that are signed and hashed.
/// Hex of the exact bytes the signature covers. For a format-v1 entry
/// these are also the bytes hashed into `content_hash_hex`; for a
/// format-v2 entry they are the signature wrapper of that hash.
pub signing_data_hex: String,
/// The entry as stored, including `previous_hash`, `runtime_key` and
/// `signature`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ use astrid_core::kernel_api::{
AuditHeadsSnapshot, AuditHealth, AuditPruneResult, AuditStats,
};
use astrid_core::{PrincipalId, SessionId, Timestamp};
use astrid_crypto::ContentHash;

use crate::Kernel;

Expand Down Expand Up @@ -467,7 +466,7 @@ fn export_entry(index: u64, entry: &AuditEntry) -> Result<AuditExportEntry, Stri
id: entry.id.0.to_string(),
timestamp: rfc3339(entry.timestamp),
previous_hash_hex: entry.previous_hash.to_hex(),
content_hash_hex: ContentHash::hash(&signing_data).to_hex(),
content_hash_hex: entry.content_hash().to_hex(),
signature_hex: entry.signature.to_hex(),
public_key_hex: entry.runtime_key.to_hex(),
signing_data_hex: hex::encode(&signing_data),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,16 @@

use std::sync::Arc;

use astrid_audit::entry_v2::signing_input;
use astrid_audit::{
AuditAction, AuditOutcome, AuditPruneReceipt, AuditRetentionPolicy, AuthorizationProof,
AuditAction, AuditEntry, AuditEntryFormat, AuditOutcome, AuditPruneReceipt,
AuditRetentionPolicy, AuthorizationProof, EntryV2Config, KeyRole,
};
use astrid_core::SessionId;
use astrid_core::dirs::AstridHome;
use astrid_core::principal::PrincipalId;
use astrid_core::profile::PrincipalProfile;
use astrid_crypto::{ContentHash, PublicKey, Signature};
use astrid_crypto::{ContentHash, KeyPair, PublicKey, Signature};
use astrid_events::ipc::{IpcMessage, IpcPayload, Topic};
use astrid_events::kernel_api::{
AUDIT_HEADS_DOMAIN_V1, AdminKernelRequest, AdminRequestKind, AdminResponseBody,
Expand Down Expand Up @@ -300,6 +302,70 @@ async fn export_pages_resume_and_entries_chain_and_verify() {
assert!(tail.complete);
}

/// Under audit format v2 the exported content hash is the v2 entry hash, the
/// value chain links and chain heads use, across the switch from v1.
#[tokio::test(flavor = "multi_thread")]
async fn export_reports_the_hash_that_links_a_chain_switched_to_v2() {
let (_dir, kernel) = fixture().await;
let session = kernel.session_id.clone();
append(&kernel, &session, Some("vera"), 2).await;
let audit_key = Arc::new(KeyPair::generate());
kernel
.audit_log
.enable_entry_v2(EntryV2Config {
audit_key: Arc::clone(&audit_key),
genesis_roles: vec![(KeyRole::AuditV1, Arc::clone(&kernel.runtime_key))],
principals: None,
})
.await
.expect("enable audit format v2");
append(&kernel, &session, Some("vera"), 3).await;

let page = export_page(&kernel, request(&session, "vera")).await;
assert!(page.complete);
assert_eq!(page.entries.len(), 5);
for (index, exported) in page.entries.iter().enumerate() {
let stored: AuditEntry = serde_json::from_value(exported.entry.clone()).unwrap();
assert_eq!(exported.content_hash_hex, stored.content_hash().to_hex());
let signing_data = hex::decode(&exported.signing_data_hex).unwrap();
assert!(verify_ed25519(
&exported.public_key_hex,
&exported.signature_hex,
&signing_data
));
if index < 2 {
assert_eq!(stored.format(), AuditEntryFormat::V1);
assert_entry_verifies(exported);
} else {
assert_eq!(stored.format(), AuditEntryFormat::V2);
assert_eq!(
exported.public_key_hex,
audit_key.export_public_key().to_hex()
);
assert_eq!(
signing_data,
signing_input(stored.content_hash().as_bytes())
);
assert_ne!(
exported.content_hash_hex,
ContentHash::hash(&signing_data).to_hex()
);
}
}
for pair in page.entries.windows(2) {
assert_eq!(pair[1].previous_hash_hex, pair[0].content_hash_hex);
}
let head = &page.entries.last().unwrap().content_hash_hex;
assert_eq!(&page.chain_head_hash_hex, head);
let snapshot = heads(&kernel).await;
let vera = snapshot
.chains
.iter()
.find(|chain| chain.principal == Some(pid("vera")))
.unwrap();
assert_eq!(&vera.head_hash_hex, head);
}

#[tokio::test(flavor = "multi_thread")]
async fn export_rejects_unknown_chains_and_foreign_cursors() {
let (_dir, kernel) = fixture().await;
Expand Down
Loading