use prikk_crypto::{ED25519_KEY_LEN, verify_ed25519};
use prikk_error::{PrikkError, Result};
use prikk_object::{ObjectEnvelope, Signature, SignatureAlgorithm, SignerRole, ascii_fold};
use crate::layout::{
LockableContainer, RepositoryLayout, validate_maintainer_key_id_storage_safety,
};
use crate::lock::{ActiveLock, acquire_container_locks};
use crate::maintainer_signing::MaintainerSigner;
use crate::trust_index::{
TrustKeyEntry, append_trust_key_entry, append_trust_policy_snapshot, lookup_trust_key_entry,
read_current_trust_policy_snapshot,
};
#[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 AdoptedMaintainerKey {
pub key_id: String,
pub public_key: [u8; ED25519_KEY_LEN],
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MaintainerTrustPolicy {
pub keys: Vec<AdoptedMaintainerKey>,
}
impl MaintainerTrustPolicy {
fn find(&self, key_id: &str) -> Option<&AdoptedMaintainerKey> {
self.keys.iter().find(|key| key.key_id == key_id)
}
}
pub fn add_trusted_maintainer(
layout: &RepositoryLayout,
key_id: &str,
public_key_hex: &str,
) -> Result<(AdoptedMaintainerKey, bool)> {
layout.require_current_format()?;
let _active_lock = ActiveLock::acquire(layout)?;
let _trust_policy_lock = acquire_container_locks(layout, &[LockableContainer::TrustPolicy])?;
crate::refs::ensure_no_incomplete_publication(layout)?;
Signature::validate_key_id(key_id)?;
validate_maintainer_key_id_storage_safety(key_id)?;
let public_key = decode_public_key_hex(public_key_hex)?;
let mut key_ids = current_adopted_key_ids(layout)?;
validate_no_maintainer_key_id_collision(&key_ids, key_id)?;
match lookup_trust_key_entry(layout, key_id)? {
Some(existing) if existing.public_key == public_key => {}
Some(_) => {
return Err(PrikkError::InvalidSignature(format!(
"maintainer key id {key_id} is already adopted with a different public key"
)));
}
None => {
append_trust_key_entry(
layout,
&TrustKeyEntry {
key_id: key_id.to_string(),
public_key,
},
)?;
}
}
let adopted = AdoptedMaintainerKey {
key_id: key_id.to_string(),
public_key,
};
if key_ids.iter().any(|existing| existing == key_id) {
return Ok((adopted, false));
}
key_ids.push(key_id.to_string());
append_trust_policy_snapshot(layout, &key_ids)?;
Ok((adopted, true))
}
pub fn remove_trusted_maintainer(layout: &RepositoryLayout, key_id: &str) -> Result<bool> {
layout.require_current_format()?;
let _active_lock = ActiveLock::acquire(layout)?;
let _trust_policy_lock = acquire_container_locks(layout, &[LockableContainer::TrustPolicy])?;
crate::refs::ensure_no_incomplete_publication(layout)?;
let mut key_ids = current_adopted_key_ids(layout)?;
let original_len = key_ids.len();
key_ids.retain(|existing| existing != key_id);
if key_ids.len() == original_len {
return Ok(false);
}
if key_ids.is_empty() {
return Err(PrikkError::Integrity(
"cannot remove the last trusted maintainer key; a repository policy must not be \
explicitly empty"
.to_string(),
));
}
append_trust_policy_snapshot(layout, &key_ids)?;
Ok(true)
}
fn current_adopted_key_ids(layout: &RepositoryLayout) -> Result<Vec<String>> {
Ok(read_current_trust_policy_snapshot(layout)?.unwrap_or_default())
}
fn validate_no_maintainer_key_id_collision(key_ids: &[String], key_id: &str) -> Result<()> {
let folded = ascii_fold(key_id);
for existing_id in key_ids {
if existing_id != key_id && ascii_fold(existing_id) == folded {
return Err(PrikkError::InvalidName(format!(
"case-insensitive maintainer key id collision involving: {existing_id}"
)));
}
}
Ok(())
}
pub fn load_maintainer_trust_policy(layout: &RepositoryLayout) -> Result<MaintainerTrustPolicy> {
let key_ids = read_current_trust_policy_snapshot(layout)?.ok_or_else(|| {
PrikkError::Integrity("publication trust policy is missing or unreadable".to_string())
})?;
let mut keys = Vec::with_capacity(key_ids.len());
for key_id in key_ids {
let entry = lookup_trust_key_entry(layout, &key_id)?.ok_or_else(|| {
PrikkError::Integrity(format!(
"trusted maintainer key {key_id} is missing or unreadable"
))
})?;
keys.push(AdoptedMaintainerKey {
key_id: entry.key_id,
public_key: entry.public_key,
});
}
Ok(MaintainerTrustPolicy { keys })
}
pub fn verify_signer_trusted(
layout: &RepositoryLayout,
signer: &impl MaintainerSigner,
) -> Result<MaintainerTrustPolicy> {
let policy = load_maintainer_trust_policy(layout)?;
let Some(matched) = policy.find(signer.key_id()) else {
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 != matched.public_key {
return Err(PrikkError::InvalidSignature(format!(
"maintainer signer public key does not match trusted key {}",
matched.key_id
)));
}
Ok(policy)
}
pub fn verify_trusted_publication_envelope(
policy: &MaintainerTrustPolicy,
envelope: &ObjectEnvelope,
) -> std::result::Result<String, PublicationTrustIssue> {
let object_id = envelope.object_id();
envelope
.signatures
.iter()
.find_map(|signature| {
verify_trusted_signature(policy, envelope, signature, object_id)
.ok()
.map(|()| signature.key_id.clone())
})
.ok_or_else(|| {
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(),
));
}
let Some(matched) = policy.find(&signature.key_id) else {
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(&matched.public_key, &preimage, &signature.signature_bytes)
}
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(all(test, target_os = "linux"))]
mod tests;