agent-toolprint 0.1.0

Double-signed receipts for AI-agent tool invocations — DSSE + JCS + Ed25519, verifiable offline (Rust port of @p-vbordei/agent-toolprint)
Documentation
//! did:key resolver and Ed25519 multicodec helpers.

use async_trait::async_trait;
use chrono::{DateTime, Utc};

use crate::error::Error;

const ED25519_MULTICODEC: [u8; 2] = [0xed, 0x01];

/// A pluggable DID resolver.
#[async_trait]
pub trait Resolver: Send + Sync {
    async fn resolve(&self, did: &str, key_id: &str, at_time: DateTime<Utc>) -> Option<Vec<u8>>;
}

pub fn parse_did_key(did: &str) -> Result<Vec<u8>, Error> {
    const PREFIX: &str = "did:key:";
    if !did.starts_with(PREFIX) {
        return Err(Error::DidKey("not a did:key".into()));
    }
    let multibase = &did[PREFIX.len()..];
    if !multibase.starts_with('z') {
        return Err(Error::DidKey(
            "did:key must use 'z' (base58btc) multibase prefix".into(),
        ));
    }
    let decoded = bs58::decode(&multibase[1..])
        .into_vec()
        .map_err(|e| Error::DidKey(format!("base58 decode: {e}")))?;
    if decoded.len() != 34
        || decoded[0] != ED25519_MULTICODEC[0]
        || decoded[1] != ED25519_MULTICODEC[1]
    {
        return Err(Error::DidKey(
            "did:key must encode an Ed25519 public key (multicodec 0xed01)".into(),
        ));
    }
    Ok(decoded[2..].to_vec())
}

pub fn did_key_from_ed25519_pubkey(pk: &[u8]) -> Result<String, Error> {
    if pk.len() != 32 {
        return Err(Error::DidKey("Ed25519 public key must be 32 bytes".into()));
    }
    let mut mc = Vec::with_capacity(34);
    mc.extend_from_slice(&ED25519_MULTICODEC);
    mc.extend_from_slice(pk);
    Ok(format!("did:key:z{}", bs58::encode(mc).into_string()))
}

/// Bundled `did:key` resolver — returns the raw public key, or None on failure.
pub struct DidKeyResolver;

#[async_trait]
impl Resolver for DidKeyResolver {
    async fn resolve(&self, did: &str, _key_id: &str, _at_time: DateTime<Utc>) -> Option<Vec<u8>> {
        parse_did_key(did).ok()
    }
}