coreason-urn-authority 0.45.1

Epistemic Ledger & OCI Trust Anchor for CoReason URNs.
Documentation
// Copyright (c) 2026 CoReason, Inc.
// All rights reserved.

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))?;

    // Extract raw Ed25519 public key bytes from multicodec-prefixed data
    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's re-exported spki/der crates for PEM encoding
    // (Zero Waste: OSS replaces hand-rolled ASN.1)
    //
    // NOTE: We build the SPKI DER structure manually rather than going through
    // VerifyingKey::from_bytes → to_public_key_pem, because from_bytes performs
    // strict Edwards-point decompression which rejects valid DID:key test vectors
    // used for non-signature contexts (e.g. W3C did:key z6Mk* examples).
    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() {
                // Try just appending .hash to filename
                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
        ));
    }

    // Constant-time comparison using ring or simple comparison
    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
        ));
    }

    // Write public key to temporary file
    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));
    }

    // Try executing ezkl CLI binary
    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 {
                // Attestation-only fallback mode
                println!("[INFO] EZKL CLI binary not found. Operating in attestation-only mode.");

                // Read and validate JSON format of proof and settings as a sanity check
                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))
            }
        }
    }
}