mod canonical_json;
#[cfg(test)]
mod tests;
use crate::diagnostic::{Diagnostic, DiagnosticCode};
use crate::model::{AdrEntry, RfcIndex, WorkItemEntry};
use canonical_json::canonicalize_json;
use serde::Serialize;
use serde_json::Value;
use sha2::{Digest, Sha256};
const SIGNATURE_VERSION: u32 = 1;
pub fn compute_rfc_signature(rfc: &RfcIndex) -> Result<String, Diagnostic> {
let mut hasher = signature_hasher("rfc");
let mut rfc_json = signature_value(
&rfc.rfc,
DiagnosticCode::E0101RfcSchemaInvalid,
"RFC",
&rfc.rfc.rfc_id,
)?;
if let Value::Object(ref mut map) = rfc_json {
map.remove("signature");
}
update_canonical_json(&mut hasher, &rfc_json);
let mut clauses: Vec<_> = rfc.clauses.iter().collect();
clauses.sort_by(|a, b| a.spec.clause_id.cmp(&b.spec.clause_id));
for clause in clauses {
let clause_json = signature_value(
&clause.spec,
DiagnosticCode::E0201ClauseSchemaInvalid,
"clause",
format!("{}:{}", rfc.rfc.rfc_id, clause.spec.clause_id),
)?;
update_canonical_json(&mut hasher, &clause_json);
}
Ok(finalize_signature(hasher))
}
pub fn compute_adr_signature(adr: &AdrEntry) -> Result<String, Diagnostic> {
compute_simple_signature(
"adr",
&adr.spec,
DiagnosticCode::E0301AdrSchemaInvalid,
"ADR",
adr.spec.govctl.id.as_str(),
)
}
pub fn compute_work_item_signature(item: &WorkItemEntry) -> Result<String, Diagnostic> {
compute_simple_signature(
"work",
&item.spec,
DiagnosticCode::E0401WorkSchemaInvalid,
"work item",
item.spec.govctl.id.as_str(),
)
}
fn compute_simple_signature<T: Serialize>(
kind: &str,
value: &T,
code: DiagnosticCode,
artifact: &str,
id: impl Into<String>,
) -> Result<String, Diagnostic> {
let mut hasher = signature_hasher(kind);
let value_json = signature_value(value, code, artifact, id)?;
update_canonical_json(&mut hasher, &value_json);
Ok(finalize_signature(hasher))
}
fn signature_hasher(kind: &str) -> Sha256 {
let mut hasher = Sha256::new();
hasher.update(format!("govctl-signature-v{SIGNATURE_VERSION}\n").as_bytes());
hasher.update(format!("type:{kind}\n").as_bytes());
hasher
}
fn signature_value<T: Serialize>(
value: &T,
code: DiagnosticCode,
artifact: &str,
id: impl Into<String>,
) -> Result<Value, Diagnostic> {
serde_json::to_value(value).map_err(|err| {
Diagnostic::new(
code,
format!("Failed to serialize {artifact} for signature: {err}"),
id,
)
})
}
fn update_canonical_json(hasher: &mut Sha256, value: &Value) {
let canonical = canonicalize_json(value);
hasher.update(canonical.as_bytes());
hasher.update(b"\n");
}
fn finalize_signature(hasher: Sha256) -> String {
let digest = hasher.finalize();
hex_encode(&digest)
}
pub fn extract_signature(markdown: &str) -> Option<String> {
for line in markdown.lines() {
let trimmed = line.trim();
if let Some(rest) = trimmed.strip_prefix("<!-- SIGNATURE: sha256:")
&& let Some(sig) = rest.strip_suffix(" -->")
{
return Some(sig.trim().to_string());
}
}
None
}
pub fn format_signature_header(source_id: &str, signature: &str) -> String {
format!(
"<!-- GENERATED: do not edit. Source: {source_id} -->\n\
<!-- SIGNATURE: sha256:{signature} -->\n"
)
}
fn hex_encode(bytes: &[u8]) -> String {
const HEX: &[u8; 16] = b"0123456789abcdef";
let mut hex = String::with_capacity(bytes.len() * 2);
for byte in bytes {
hex.push(HEX[(byte >> 4) as usize] as char);
hex.push(HEX[(byte & 0x0f) as usize] as char);
}
hex
}
pub fn is_rfc_amended(rfc: &RfcIndex) -> bool {
let Some(stored_sig) = &rfc.rfc.signature else {
return false;
};
let Ok(current_sig) = compute_rfc_signature(rfc) else {
return false;
};
stored_sig != ¤t_sig
}