use prikk_crypto::{ED25519_KEY_LEN, verify_ed25519};
use prikk_error::{PrikkError, Result};
use prikk_hash::sha256;
use prikk_object::{ObjectEnvelope, Signature, SignatureAlgorithm, SignerRole};
use std::path::Path;
use crate::byte_cursor::ByteCursor;
use crate::file_codec::push_string_u16;
use crate::frame_resync::resync_to_next_magic;
use crate::fsutil::{
append_file_required, create_new_file_required, len_to_u64, read_file_if_exists,
};
use crate::layout::RepositoryLayout;
use crate::lock::ActiveLock;
const AUTHOR_KEY_MAGIC: &[u8; 8] = b"PAUTKEY1";
const AUTHOR_KEY_VERSION: u16 = 1;
const AUTHOR_KEY_HEADER_LEN: usize = 8 + 2 + 8 + 32;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct AuthorKeyEntry {
pub(crate) key_id: String,
pub(crate) public_key: [u8; ED25519_KEY_LEN],
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum AuthorKeyRecordStatus {
Evaluated,
Failed { message: String },
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct AuthorKeyRecordOutcome {
pub(crate) offset: usize,
pub(crate) status: AuthorKeyRecordStatus,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct AuthorKeyReplay {
pub(crate) entries: Vec<AuthorKeyEntry>,
pub(crate) trailing_partial_bytes: usize,
pub(crate) record_outcomes: Vec<AuthorKeyRecordOutcome>,
}
impl AuthorKeyReplay {
#[must_use]
pub(crate) fn has_item_failure(&self) -> bool {
self.record_outcomes
.iter()
.any(|outcome| matches!(outcome.status, AuthorKeyRecordStatus::Failed { .. }))
}
}
fn encode_author_key_body(entry: &AuthorKeyEntry) -> Result<Vec<u8>> {
let mut body = Vec::new();
push_string_u16(&mut body, &entry.key_id)?;
body.extend_from_slice(&entry.public_key);
Ok(body)
}
fn decode_author_key_body(body: &[u8]) -> Result<AuthorKeyEntry> {
let mut cursor = ByteCursor::new(body);
let key_id = cursor.read_string_u16()?;
let public_key = cursor.read_array::<ED25519_KEY_LEN>()?;
if !cursor.is_finished() {
return Err(PrikkError::MalformedData(
"trailing bytes in author key entry body".to_string(),
));
}
Ok(AuthorKeyEntry { key_id, public_key })
}
fn encode_author_key_record(entry: &AuthorKeyEntry) -> Result<Vec<u8>> {
let body = encode_author_key_body(entry)?;
let body_len = len_to_u64(body.len())?;
let checksum = author_key_checksum(body_len, &body);
let mut out = Vec::with_capacity(AUTHOR_KEY_HEADER_LEN + body.len());
out.extend_from_slice(AUTHOR_KEY_MAGIC);
crate::file_codec::push_u16(&mut out, AUTHOR_KEY_VERSION);
out.extend_from_slice(&body_len.to_be_bytes());
out.extend_from_slice(&checksum);
out.extend_from_slice(&body);
Ok(out)
}
fn author_key_checksum(body_len: u64, body: &[u8]) -> [u8; 32] {
let mut preimage = Vec::new();
preimage.extend_from_slice(AUTHOR_KEY_MAGIC);
preimage.extend_from_slice(&AUTHOR_KEY_VERSION.to_be_bytes());
preimage.extend_from_slice(&body_len.to_be_bytes());
preimage.extend_from_slice(body);
sha256(&preimage)
}
enum AuthorKeyFrameAttempt {
Record {
entry: AuthorKeyEntry,
next_offset: usize,
},
TrailingPartial {
remaining: usize,
},
Invalid {
message: String,
},
}
fn parse_author_key_frame_at(bytes: &[u8], offset: usize) -> AuthorKeyFrameAttempt {
let remaining = bytes.len().saturating_sub(offset);
if remaining < AUTHOR_KEY_HEADER_LEN {
return AuthorKeyFrameAttempt::TrailingPartial { remaining };
}
let header_end = offset + AUTHOR_KEY_HEADER_LEN;
let Some(header) = bytes.get(offset..header_end) else {
return AuthorKeyFrameAttempt::TrailingPartial { remaining };
};
let mut cursor = ByteCursor::new(header);
let (magic, version, body_len, checksum) = match (|| -> Result<_> {
let magic = cursor.read_array::<8>()?;
let version = cursor.read_u16()?;
let body_len = cursor.read_u64()?;
let checksum = cursor.read_array::<32>()?;
Ok((magic, version, body_len, checksum))
})() {
Ok(values) => values,
Err(err) => {
return AuthorKeyFrameAttempt::Invalid {
message: err.to_string(),
};
}
};
if &magic != AUTHOR_KEY_MAGIC {
return AuthorKeyFrameAttempt::Invalid {
message: "invalid author key record magic".to_string(),
};
}
if version != AUTHOR_KEY_VERSION {
return AuthorKeyFrameAttempt::Invalid {
message: format!("unsupported author key record version {version}"),
};
}
let Ok(body_len_usize) = usize::try_from(body_len) else {
return AuthorKeyFrameAttempt::Invalid {
message: "author key body length does not fit usize".to_string(),
};
};
let Some(body_end) = header_end.checked_add(body_len_usize) else {
return AuthorKeyFrameAttempt::Invalid {
message: "author key body end overflow".to_string(),
};
};
let Some(body) = bytes.get(header_end..body_end) else {
return AuthorKeyFrameAttempt::TrailingPartial { remaining };
};
let expected = author_key_checksum(body_len, body);
if expected != checksum {
return AuthorKeyFrameAttempt::Invalid {
message: format!("author key checksum mismatch at byte offset {offset}"),
};
}
match decode_author_key_body(body) {
Ok(entry) => AuthorKeyFrameAttempt::Record {
entry,
next_offset: body_end,
},
Err(err) => AuthorKeyFrameAttempt::Invalid {
message: err.to_string(),
},
}
}
fn decode_author_key_records(bytes: &[u8]) -> Result<AuthorKeyReplay> {
let mut entries = Vec::new();
let mut record_outcomes = Vec::new();
let mut offset = 0_usize;
loop {
match parse_author_key_frame_at(bytes, offset) {
AuthorKeyFrameAttempt::Record { entry, next_offset } => {
record_outcomes.push(AuthorKeyRecordOutcome {
offset,
status: AuthorKeyRecordStatus::Evaluated,
});
entries.push(entry);
offset = next_offset;
}
AuthorKeyFrameAttempt::TrailingPartial { remaining } => {
return Ok(AuthorKeyReplay {
entries,
trailing_partial_bytes: remaining,
record_outcomes,
});
}
AuthorKeyFrameAttempt::Invalid { message } => {
record_outcomes.push(AuthorKeyRecordOutcome {
offset,
status: AuthorKeyRecordStatus::Failed { message },
});
match resync_to_next_magic(bytes, offset + 1, AUTHOR_KEY_MAGIC.as_slice()) {
Some(next) => offset = next,
None => {
return Ok(AuthorKeyReplay {
entries,
trailing_partial_bytes: 0,
record_outcomes,
});
}
}
}
}
}
}
fn replay_author_keys(layout: &RepositoryLayout) -> Result<AuthorKeyReplay> {
let relative = layout.repository_relative(&layout.author_key_container_path())?;
let Some(bytes) = read_file_if_exists(layout.repository_mutation_root(), &relative)? else {
return Ok(AuthorKeyReplay {
entries: Vec::new(),
trailing_partial_bytes: 0,
record_outcomes: Vec::new(),
});
};
decode_author_key_records(&bytes)
}
pub(crate) fn lookup_author_key_entries(
layout: &RepositoryLayout,
key_id: &str,
) -> Result<Vec<AuthorKeyEntry>> {
let replay = replay_author_keys(layout)?;
if replay.has_item_failure() {
return Err(PrikkError::Integrity(
"author key container has a damaged entry; run doctor before reading".to_string(),
));
}
Ok(replay
.entries
.into_iter()
.filter(|entry| entry.key_id == key_id)
.collect())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum AuthorKeyCheck {
AlreadyRecorded,
New,
}
pub(crate) fn check_author_key_conflict(
layout: &RepositoryLayout,
key_id: &str,
public_key: [u8; ED25519_KEY_LEN],
) -> Result<AuthorKeyCheck> {
let existing = lookup_author_key_entries(layout, key_id)?;
if existing.iter().any(|entry| entry.public_key == public_key) {
return Ok(AuthorKeyCheck::AlreadyRecorded);
}
if let Some(conflicting) = existing.first() {
return Err(PrikkError::Integrity(format!(
"author key_id {key_id} already has a different recorded public key ({}); one key_id \
binds to one public key -- this looks like a key-rotation attempt, which is not \
supported and is indistinguishable from impersonation",
prikk_hash::to_hex(&conflicting.public_key)
)));
}
Ok(AuthorKeyCheck::New)
}
pub(crate) fn record_author_key_material(
layout: &RepositoryLayout,
key_id: &str,
public_key: [u8; ED25519_KEY_LEN],
_active_lock: &ActiveLock,
) -> Result<()> {
if check_author_key_conflict(layout, key_id, public_key)? == AuthorKeyCheck::AlreadyRecorded {
return Ok(());
}
let record = encode_author_key_record(&AuthorKeyEntry {
key_id: key_id.to_string(),
public_key,
})?;
let relative = layout.repository_relative(&layout.author_key_container_path())?;
ensure_author_key_container_exists(layout, &relative)?;
append_file_required(layout.repository_mutation_root(), &relative, &record)
}
fn ensure_author_key_container_exists(layout: &RepositoryLayout, relative: &Path) -> Result<()> {
if read_file_if_exists(layout.repository_mutation_root(), relative)?.is_some() {
return Ok(());
}
match create_new_file_required(layout.repository_mutation_root(), relative, &[]) {
Ok(()) => Ok(()),
Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => Ok(()),
Err(err) => Err(err.into()),
}
}
#[cfg(test)]
pub(crate) fn force_conflicting_author_key_entry_for_test(
layout: &RepositoryLayout,
key_id: &str,
public_key: [u8; ED25519_KEY_LEN],
) -> Result<()> {
let record = encode_author_key_record(&AuthorKeyEntry {
key_id: key_id.to_string(),
public_key,
})?;
let relative = layout.repository_relative(&layout.author_key_container_path())?;
ensure_author_key_container_exists(layout, &relative)?;
append_file_required(layout.repository_mutation_root(), &relative, &record)
}
pub(crate) fn verify_author_signature(
layout: &RepositoryLayout,
envelope: &ObjectEnvelope,
) -> Result<Option<(String, bool)>> {
let Some(signature) = envelope
.signatures
.iter()
.find(|signature| signature.signer_role == SignerRole::Author)
else {
return Ok(None);
};
let entries = lookup_author_key_entries(layout, &signature.key_id)?;
verify_author_signature_against_material(envelope, &entries)
}
pub(crate) fn verify_author_signature_against_material(
envelope: &ObjectEnvelope,
candidate_entries: &[AuthorKeyEntry],
) -> Result<Option<(String, bool)>> {
let Some(signature) = envelope
.signatures
.iter()
.find(|signature| signature.signer_role == SignerRole::Author)
else {
return Ok(None);
};
if signature.algorithm != SignatureAlgorithm::Ed25519 {
return Err(PrikkError::InvalidSignature(
"AUTHOR signature is not Ed25519".to_string(),
));
}
if candidate_entries.is_empty() {
return Ok(Some((signature.key_id.clone(), false)));
}
let first_public_key = candidate_entries.first().map(|entry| entry.public_key);
if candidate_entries
.iter()
.any(|entry| Some(entry.public_key) != first_public_key)
{
return Err(PrikkError::Integrity(format!(
"author key_id {} has more than one distinct recorded public key -- authorship \
integrity for this key_id cannot be established",
signature.key_id
)));
}
let preimage = Signature::signed_bytes(
SignatureAlgorithm::Ed25519,
envelope.object_type,
envelope.object_id(),
SignerRole::Author,
&signature.key_id,
)?;
let verifies = candidate_entries.iter().any(|entry| {
verify_ed25519(&entry.public_key, &preimage, &signature.signature_bytes).is_ok()
});
if !verifies {
return Err(PrikkError::InvalidSignature(format!(
"{} {} AUTHOR signature does not verify against recorded key material for {}",
envelope.object_type,
envelope.object_id(),
signature.key_id
)));
}
Ok(Some((signature.key_id.clone(), true)))
}
#[cfg(test)]
mod tests;