dyolo-kya 1.0.0

Know Your Agent (KYA): cryptographic chain-of-custody for recursive AI delegation with provable scope narrowing
use blake3::Hasher;
use ed25519_dalek::{Signature, Verifier, VerifyingKey};

use crate::crypto::{DOMAIN_CERT_FP, DOMAIN_CERT_SIG};
use crate::identity::DyoloIdentity;
use crate::intent::IntentHash;
use crate::registry::fresh_nonce;
use crate::SubScopeProof;

/// A single cryptographically sealed delegation hop.
///
/// One `DelegationCert` represents the act of `delegator` granting
/// `delegate` the authority to act within `scope_root`, optionally
/// restricting that scope to a proven subset via `scope_proof`.
///
/// The Ed25519 signature covers every field including the `SubScopeProof`
/// commitment, making it impossible to swap or forge the scope proof on
/// an existing certificate.
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct DelegationCert {
    /// The identity granting this delegation.
    pub delegator_pk: VerifyingKey,
    /// The identity receiving this delegation.
    pub delegate_pk: VerifyingKey,
    /// The Merkle root of the authorized intent set for `delegate`.
    pub scope_root: IntentHash,
    /// Proof that `scope_root` is a valid subset of the delegator's scope.
    pub scope_proof: SubScopeProof,
    /// Replay-prevention token. Must be globally unique per chain.
    pub nonce: [u8; 16],
    /// Unix timestamp at which this delegation becomes valid.
    pub issued_at: u64,
    /// Unix timestamp at which this delegation expires.
    pub expiration_unix: u64,
    /// Maximum number of further delegation hops permitted below this cert.
    pub max_depth: u8,
    /// Ed25519 signature by `delegator_pk` over all of the above.
    pub signature: Signature,
}

impl DelegationCert {
    #[inline(always)]
    pub(crate) fn signable_bytes(
        delegator_pk: &VerifyingKey,
        delegate_pk: &VerifyingKey,
        scope_root: &IntentHash,
        scope_proof: &SubScopeProof,
        nonce: &[u8; 16],
        issued_at: u64,
        expiration_unix: u64,
        max_depth: u8,
    ) -> Vec<u8> {
        let domain_bytes = DOMAIN_CERT_SIG.as_bytes();
        let exact_len = domain_bytes.len() + 32 + 32 + 32 + 32 + 16 + 8 + 8 + 1;

        let mut m = Vec::with_capacity(exact_len);
        m.extend_from_slice(domain_bytes);
        m.extend_from_slice(delegator_pk.as_bytes());
        m.extend_from_slice(delegate_pk.as_bytes());
        m.extend_from_slice(scope_root);
        m.extend_from_slice(&scope_proof.commitment());
        m.extend_from_slice(nonce);
        m.extend_from_slice(&issued_at.to_be_bytes());
        m.extend_from_slice(&expiration_unix.to_be_bytes());
        m.push(max_depth);

        debug_assert_eq!(
            m.len(),
            exact_len,
            "signable_bytes length mismatch: expected {exact_len}, got {}",
            m.len()
        );
        m
    }

    pub(crate) fn issue(
        delegator: &DyoloIdentity,
        delegate_pk: VerifyingKey,
        scope_root: IntentHash,
        scope_proof: SubScopeProof,
        nonce: [u8; 16],
        issued_at: u64,
        expiration_unix: u64,
        max_depth: u8,
    ) -> Self {
        let delegator_pk = delegator.verifying_key();
        let msg = Self::signable_bytes(
            &delegator_pk,
            &delegate_pk,
            &scope_root,
            &scope_proof,
            &nonce,
            issued_at,
            expiration_unix,
            max_depth,
        );
        Self {
            delegator_pk,
            delegate_pk,
            scope_root,
            scope_proof,
            nonce,
            issued_at,
            expiration_unix,
            max_depth,
            signature: delegator.sign(&msg),
        }
    }

    pub(crate) fn verify_signature(&self) -> bool {
        let msg = Self::signable_bytes(
            &self.delegator_pk,
            &self.delegate_pk,
            &self.scope_root,
            &self.scope_proof,
            &self.nonce,
            self.issued_at,
            self.expiration_unix,
            self.max_depth,
        );
        self.delegator_pk.verify(&msg, &self.signature).is_ok()
    }

    /// A 32-byte fingerprint of this certificate, suitable as a revocation key
    /// or an audit log entry identifier.
    #[must_use]
    pub fn fingerprint(&self) -> [u8; 32] {
        let mut h = Hasher::new_derive_key(DOMAIN_CERT_FP);
        h.update(&self.signature.to_bytes());
        h.finalize().into()
    }
}

// ── Certificate Builder ───────────────────────────────────────────────────────

/// Ergonomic builder for [`DelegationCert`].
///
/// # Examples
///
/// ```rust,ignore
/// // Full-scope delegation:
/// let cert = CertBuilder::new(agent.verifying_key(), scope_root, now, expiry)
///     .sign(&delegator);
///
/// // Narrowed scope with a depth cap:
/// let proof = SubScopeProof::build(&parent_tree, &[trade_intent])?;
/// let cert = CertBuilder::new(agent.verifying_key(), sub_root, now, expiry)
///     .scope_proof(proof)
///     .max_depth(4)
///     .sign(&delegator);
/// ```
pub struct CertBuilder {
    delegate_pk:     VerifyingKey,
    scope_root:      IntentHash,
    scope_proof:     SubScopeProof,
    nonce:           [u8; 16],
    issued_at:       u64,
    expiration_unix: u64,
    max_depth:       u8,
}

impl CertBuilder {
    /// Start building a certificate. A fresh nonce is generated automatically.
    pub fn new(
        delegate_pk: VerifyingKey,
        scope_root: IntentHash,
        issued_at: u64,
        expiration_unix: u64,
    ) -> Self {
        Self {
            delegate_pk,
            scope_root,
            scope_proof: SubScopeProof::full_passthrough(),
            nonce: fresh_nonce(),
            issued_at,
            expiration_unix,
            max_depth: 16,
        }
    }

    /// Attach a sub-scope proof. If omitted, the cert uses full-scope pass-through.
    pub fn scope_proof(mut self, proof: SubScopeProof) -> Self {
        self.scope_proof = proof;
        self
    }

    /// Override the auto-generated nonce. Use only in tests with a fixed value.
    pub fn nonce(mut self, nonce: [u8; 16]) -> Self {
        self.nonce = nonce;
        self
    }

    /// Set the maximum number of further delegation hops below this certificate.
    /// Defaults to `16`. Set to `0` to produce a terminal (leaf-only) delegation.
    pub fn max_depth(mut self, depth: u8) -> Self {
        self.max_depth = depth;
        self
    }

    /// Sign and produce the final certificate.
    pub fn sign(self, delegator: &DyoloIdentity) -> DelegationCert {
        DelegationCert::issue(
            delegator,
            self.delegate_pk,
            self.scope_root,
            self.scope_proof,
            self.nonce,
            self.issued_at,
            self.expiration_unix,
            self.max_depth,
        )
    }
}