use sha2::{Digest, Sha256};
use std::fs;
use std::io::Write;
use std::path::Path;
use tempfile::NamedTempFile;
pub fn extract_public_key_pem_from_did(did_key: &str) -> Result<String, String> {
if !did_key.starts_with("did:key:z") {
let got = if did_key.len() > 20 {
&did_key[..20]
} else {
did_key
};
return Err(format!(
"Invalid DID format: expected 'did:key:z...' prefix, got '{}...'",
got
));
}
let suffix = &did_key["did:key:z".len()..];
let decoded = bs58::decode(suffix)
.into_vec()
.map_err(|e| format!("Failed to base58-decode DID key material: {}", e))?;
let raw_pubkey = if decoded.starts_with(b"\xed\x01") {
&decoded[2..]
} else if decoded.starts_with(b"\x0c\x2a") {
if decoded.len() < 34 {
return Err(format!(
"Invalid custom key length: expected at least 34 bytes, got {}",
decoded.len()
));
}
&decoded[decoded.len() - 32..]
} else {
let got_hex = if decoded.len() >= 2 {
format!("{:02x}{:02x}", decoded[0], decoded[1])
} else {
hex::encode(&decoded)
};
return Err(format!(
"Unsupported key type: expected Ed25519 multicodec prefix (0xed01), got {}",
got_hex
));
};
if raw_pubkey.len() != 32 {
return Err(format!(
"Invalid Ed25519 key length: expected 32 bytes, got {}",
raw_pubkey.len()
));
}
use ed25519_dalek::pkcs8::spki::der::pem::LineEnding;
use ed25519_dalek::pkcs8::spki::der::EncodePem;
use ed25519_dalek::pkcs8::spki::{AlgorithmIdentifierOwned, SubjectPublicKeyInfoOwned};
let algorithm = AlgorithmIdentifierOwned {
oid: ed25519_dalek::pkcs8::spki::ObjectIdentifier::new_unwrap("1.3.101.112"),
parameters: None,
};
let spki_doc = SubjectPublicKeyInfoOwned {
algorithm,
subject_public_key: ed25519_dalek::pkcs8::spki::der::asn1::BitString::from_bytes(
raw_pubkey,
)
.map_err(|e| format!("Failed to create BitString: {}", e))?,
};
let pem = spki_doc
.to_pem(LineEnding::LF)
.map_err(|e| format!("Failed to encode PEM: {}", e))?;
Ok(pem)
}
pub fn compute_canonical_hash(payload: &str) -> String {
let normalized = payload.replace("\r\n", "\n").replace('\r', "\n");
let mut hasher = Sha256::new();
hasher.update(normalized.as_bytes());
hex::encode(hasher.finalize())
}
pub fn verify_code_attestation(
code_path: &Path,
expected_hash: Option<&str>,
) -> Result<(), String> {
if !code_path.exists() {
return Err(format!(
"Attestation target code file does not exist: {:?}",
code_path
));
}
let content = fs::read_to_string(code_path)
.map_err(|e| format!("Failed to read target file {:?}: {}", code_path, e))?;
let actual_hash = compute_canonical_hash(&content);
let target_hash = match expected_hash {
Some(h) => h.to_string(),
None => {
let mut sidecar = code_path.to_path_buf();
if let Some(ext) = code_path.extension() {
sidecar.set_extension(format!("{}.hash", ext.to_string_lossy()));
} else {
sidecar.set_extension("hash");
}
if !sidecar.exists() {
let direct_sidecar =
code_path
.parent()
.unwrap_or_else(|| Path::new(""))
.join(format!(
"{}.hash",
code_path.file_name().unwrap().to_str().unwrap()
));
if direct_sidecar.exists() {
fs::read_to_string(direct_sidecar)
.map_err(|e| e.to_string())?
.trim()
.to_string()
} else {
return Err(format!(
"No sidecar hash file found for code path: {:?}",
code_path
));
}
} else {
fs::read_to_string(&sidecar)
.map_err(|e| e.to_string())?
.trim()
.to_string()
}
}
};
if target_hash.is_empty() {
return Err(format!(
"Empty expected hash for code verification of {:?}",
code_path
));
}
if actual_hash != target_hash {
return Err(format!(
"Zero-Trust Attestation Handshake Failed: Code signature mismatch for '{:?}'. Expected: {}..., Actual: {}...",
code_path.file_name().unwrap_or_default(),
&target_hash[..std::cmp::min(16, target_hash.len())],
&actual_hash[..std::cmp::min(16, actual_hash.len())]
));
}
Ok(())
}
pub fn verify_oci_signature(oci_uri: &str, public_key_pem: &str) -> Result<(), String> {
if oci_uri.trim().starts_with('-') {
return Err(format!(
"Security Exception: Malformed or hostile OCI URI format detected: {}",
oci_uri
));
}
let mut key_file =
NamedTempFile::new().map_err(|e| format!("Failed to create temporary key file: {}", e))?;
key_file
.write_all(public_key_pem.as_bytes())
.map_err(|e| format!("Failed to write public key to temporary file: {}", e))?;
let key_path = key_file.path();
let output = std::process::Command::new("cosign")
.args([
"verify",
"--key",
key_path.to_str().unwrap(),
"--output",
"json",
"--",
oci_uri,
])
.output();
let result = match output {
Ok(out) => out,
Err(e) => {
if e.kind() == std::io::ErrorKind::NotFound {
return Err("Cosign binary not found. Install cosign: https://docs.sigstore.dev/cosign/system_config/installation/".to_string());
}
return Err(format!("Failed to execute cosign check: {}", e));
}
};
if !result.status.success() {
let err_msg = String::from_utf8_lossy(&result.stderr).trim().to_string();
return Err(format!(
"OCI signature verification failed for '{}': {}",
oci_uri, err_msg
));
}
Ok(())
}
pub fn verify_ezkl_proof(
proof_path: &Path,
vk_path: &Path,
settings_path: &Path,
) -> Result<bool, String> {
if !proof_path.exists() {
return Err(format!("Proof file does not exist: {:?}", proof_path));
}
if !vk_path.exists() {
return Err(format!(
"Verification key file does not exist: {:?}",
vk_path
));
}
if !settings_path.exists() {
return Err(format!("Settings file does not exist: {:?}", settings_path));
}
let output = std::process::Command::new("ezkl")
.args([
"verify",
"--proof",
proof_path.to_str().unwrap(),
"--vk",
vk_path.to_str().unwrap(),
"--settings",
settings_path.to_str().unwrap(),
])
.output();
match output {
Ok(out) => {
if out.status.success() {
Ok(true)
} else {
let err_msg = String::from_utf8_lossy(&out.stderr).trim().to_string();
Err(format!("EZKL verification failed: {}", err_msg))
}
}
Err(e) => {
if e.kind() == std::io::ErrorKind::NotFound {
println!("[INFO] EZKL CLI binary not found. Operating in attestation-only mode.");
let proof_content = fs::read_to_string(proof_path)
.map_err(|err| format!("Failed to read proof: {}", err))?;
let _proof_json: serde_json::Value = serde_json::from_str(&proof_content)
.map_err(|err| format!("Invalid JSON format in proof: {}", err))?;
let settings_content = fs::read_to_string(settings_path)
.map_err(|err| format!("Failed to read settings: {}", err))?;
let _settings_json: serde_json::Value = serde_json::from_str(&settings_content)
.map_err(|err| format!("Invalid JSON format in settings: {}", err))?;
Ok(true)
} else {
Err(format!("Failed to execute ezkl check: {}", e))
}
}
}
}