use std::fs;
use prikk_crypto::{ED25519_KEY_LEN, verify_ed25519};
use prikk_error::{PrikkError, Result};
use prikk_hash::to_hex;
use prikk_object::{ObjectEnvelope, Signature, SignatureAlgorithm, SignerRole};
use crate::fsutil::{sync_directory_best_effort, write_file_atomically};
use crate::layout::RepositoryLayout;
use crate::maintainer_signing::MaintainerSigner;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PublicationTrustIssue {
pub code: &'static str,
pub message: String,
}
impl PublicationTrustIssue {
#[must_use]
pub fn new(code: &'static str, message: impl Into<String>) -> Self {
Self {
code,
message: message.into(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MaintainerTrustPolicy {
pub key_id: String,
pub public_key: [u8; ED25519_KEY_LEN],
}
pub fn add_trusted_maintainer(
layout: &RepositoryLayout,
key_id: &str,
public_key_hex: &str,
) -> Result<MaintainerTrustPolicy> {
Signature::validate_key_id(key_id)?;
let public_key = decode_public_key_hex(public_key_hex)?;
fs::create_dir_all(layout.maintainer_trust_keys_dir())?;
let key_path = layout.maintainer_trust_key_path(key_id)?;
let public_key_text = format!("{}\n", to_hex(&public_key));
write_file_atomically(&key_path, public_key_text.as_bytes())?;
if let Some(parent) = key_path.parent() {
sync_directory_best_effort(parent)?;
}
let policy_text = format!("[maintainer]\nrequired = 1\nkeys = [\"{key_id}\"]\n");
write_file_atomically(&layout.trust_policy_path(), policy_text.as_bytes())?;
sync_directory_best_effort(&layout.trust_dir())?;
Ok(MaintainerTrustPolicy {
key_id: key_id.to_string(),
public_key,
})
}
pub fn load_maintainer_trust_policy(layout: &RepositoryLayout) -> Result<MaintainerTrustPolicy> {
let policy_text = fs::read_to_string(layout.trust_policy_path()).map_err(|err| {
PrikkError::Integrity(format!(
"publication trust policy is missing or unreadable: {err}"
))
})?;
let key_id = parse_policy_key_id(&policy_text)?;
Signature::validate_key_id(&key_id)?;
let key_path = layout.maintainer_trust_key_path(&key_id)?;
let public_key_text = fs::read_to_string(&key_path).map_err(|err| {
PrikkError::Integrity(format!(
"trusted maintainer key {key_id} is missing or unreadable: {err}"
))
})?;
let public_key = decode_public_key_hex(public_key_text.trim_end_matches('\n'))?;
Ok(MaintainerTrustPolicy { key_id, public_key })
}
pub fn verify_signer_trusted(
layout: &RepositoryLayout,
signer: &impl MaintainerSigner,
) -> Result<MaintainerTrustPolicy> {
let policy = load_maintainer_trust_policy(layout)?;
if signer.key_id() != policy.key_id {
return Err(PrikkError::InvalidSignature(format!(
"maintainer signer key id {} is not trusted by policy",
signer.key_id()
)));
}
let signer_public_key = signer.public_key_bytes();
if signer_public_key != policy.public_key {
return Err(PrikkError::InvalidSignature(format!(
"maintainer signer public key does not match trusted key {}",
policy.key_id
)));
}
Ok(policy)
}
pub fn verify_trusted_publication_envelope(
policy: &MaintainerTrustPolicy,
envelope: &ObjectEnvelope,
) -> std::result::Result<(), PublicationTrustIssue> {
let object_id = envelope.object_id();
let trusted = envelope
.signatures
.iter()
.any(|signature| verify_trusted_signature(policy, envelope, signature, object_id).is_ok());
if trusted {
Ok(())
} else {
Err(PublicationTrustIssue::new(
"PRIKK-TRUST-PUBLICATION-UNTRUSTED",
format!(
"{} {} has no trusted MAINTAINER signature",
envelope.object_type, object_id
),
))
}
}
fn verify_trusted_signature(
policy: &MaintainerTrustPolicy,
envelope: &ObjectEnvelope,
signature: &Signature,
object_id: prikk_object::ObjectId,
) -> Result<()> {
if signature.algorithm != SignatureAlgorithm::Ed25519 {
return Err(PrikkError::InvalidSignature(
"publication signature is not Ed25519".to_string(),
));
}
if signature.signer_role != SignerRole::Maintainer {
return Err(PrikkError::InvalidSignature(
"publication signature role is not MAINTAINER".to_string(),
));
}
if signature.key_id != policy.key_id {
return Err(PrikkError::InvalidSignature(
"publication signature key id is not trusted".to_string(),
));
}
let preimage = Signature::signed_bytes(
SignatureAlgorithm::Ed25519,
envelope.object_type,
object_id,
SignerRole::Maintainer,
&signature.key_id,
)?;
verify_ed25519(&policy.public_key, &preimage, &signature.signature_bytes)
}
fn parse_policy_key_id(policy_text: &str) -> Result<String> {
let lines: Vec<&str> = policy_text
.lines()
.map(str::trim)
.filter(|line| !line.is_empty())
.collect();
if lines.len() != 3 {
return Err(PrikkError::MalformedData(
"trust policy must contain exactly [maintainer], required, and keys lines".to_string(),
));
}
let Some(section) = lines.first() else {
return Err(PrikkError::MalformedData(
"trust policy is empty".to_string(),
));
};
if *section != "[maintainer]" {
return Err(PrikkError::MalformedData(
"trust policy must start with [maintainer]".to_string(),
));
}
let Some(required) = lines.get(1) else {
return Err(PrikkError::MalformedData(
"trust policy missing required line".to_string(),
));
};
if *required != "required = 1" {
return Err(PrikkError::MalformedData(
"trust policy must set required = 1".to_string(),
));
}
let Some(keys) = lines.get(2) else {
return Err(PrikkError::MalformedData(
"trust policy missing keys line".to_string(),
));
};
let Some(rest) = keys.strip_prefix("keys = [\"") else {
return Err(PrikkError::MalformedData(
"trust policy keys line must be keys = [\"<key-id>\"]".to_string(),
));
};
let Some(key_id) = rest.strip_suffix("\"]") else {
return Err(PrikkError::MalformedData(
"trust policy keys line must contain exactly one key".to_string(),
));
};
Signature::validate_key_id(key_id)?;
Ok(key_id.to_string())
}
fn decode_public_key_hex(hex: &str) -> Result<[u8; ED25519_KEY_LEN]> {
if hex.len() != ED25519_KEY_LEN * 2 {
return Err(PrikkError::MalformedData(format!(
"maintainer public key must be {} lowercase hex characters",
ED25519_KEY_LEN * 2
)));
}
if !hex.bytes().all(|byte| byte.is_ascii_hexdigit()) {
return Err(PrikkError::MalformedData(
"maintainer public key contains non-hex bytes".to_string(),
));
}
if hex.bytes().any(|byte| byte.is_ascii_uppercase()) {
return Err(PrikkError::MalformedData(
"maintainer public key must use lowercase hex".to_string(),
));
}
let mut out = [0_u8; ED25519_KEY_LEN];
for (slot, pair) in out.iter_mut().zip(hex.as_bytes().chunks_exact(2)) {
let hi =
hex_value(pair.first().copied().ok_or_else(|| {
PrikkError::MalformedData("truncated public key hex".to_string())
})?)?;
let lo =
hex_value(pair.get(1).copied().ok_or_else(|| {
PrikkError::MalformedData("truncated public key hex".to_string())
})?)?;
*slot = (hi << 4) | lo;
}
Ok(out)
}
fn hex_value(byte: u8) -> Result<u8> {
match byte {
b'0'..=b'9' => Ok(byte - b'0'),
b'a'..=b'f' => Ok(byte - b'a' + 10),
_ => Err(PrikkError::MalformedData(
"maintainer public key must use lowercase hex".to_string(),
)),
}
}
#[cfg(test)]
mod tests;