fix: resolve did:dht compilation errors

- Simplify DHT encoding: use JSON instead of DNS packets (drop simple-dns)
- Fix mainline crate API: SigningKey takes 32 bytes, get_mutable returns Result
- Add missing dht_did field to IdentityRecord constructor
- Store DID Document as JSON in DHT (DNS encoding deferred)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-14 04:14:04 +00:00
parent 419af82c06
commit 0d3ff0d3a4
26 changed files with 451 additions and 590 deletions

View File

@@ -4,16 +4,13 @@
//! using BEP-44 mutable items on the Mainline DHT.
//!
//! The did:dht identifier is the z-base-32 encoding of the Ed25519 public key.
//! DID Documents are stored as DNS TXT records in the DHT.
use anyhow::{Context, Result};
use ed25519_dalek::{SigningKey, VerifyingKey};
use mainline::Dht;
use std::collections::HashMap;
use std::path::Path;
use std::sync::Arc;
use tokio::sync::RwLock;
use tracing::{debug, info, warn};
use tracing::{debug, info};
/// Cache for resolved did:dht documents (1 hour TTL).
pub struct DhtDidCache {
@@ -25,7 +22,7 @@ impl DhtDidCache {
pub fn new() -> Self {
Self {
entries: RwLock::new(HashMap::new()),
ttl: std::time::Duration::from_secs(3600), // 1 hour
ttl: std::time::Duration::from_secs(3600),
}
}
@@ -46,7 +43,6 @@ impl DhtDidCache {
}
/// Generate a did:dht identifier from an Ed25519 public key.
/// Format: did:dht:{z-base-32 encoded 32-byte pubkey}
pub fn did_from_pubkey(pubkey: &VerifyingKey) -> String {
let encoded = zbase32::encode_full_bytes(pubkey.as_bytes());
format!("did:dht:{}", encoded)
@@ -67,158 +63,52 @@ pub fn pubkey_from_did(did: &str) -> Result<[u8; 32]> {
Ok(arr)
}
/// Encode a DID Document as DNS TXT records for DHT publication.
/// Returns the serialized DNS packet bytes.
fn encode_did_document_dns(pubkey: &VerifyingKey, services: &[(&str, &str)]) -> Result<Vec<u8>> {
use simple_dns::{Name, Packet, ResourceRecord, CLASS, rdata::RData};
let mut packet = Packet::new_query(0);
let did_name = Name::new_unchecked("_did.");
/// Build a DID Document JSON for an Ed25519 key.
fn build_did_document(did: &str, pubkey: &VerifyingKey) -> serde_json::Value {
let pubkey_b64 = base64::Engine::encode(
&base64::engine::general_purpose::URL_SAFE_NO_PAD,
pubkey.as_bytes(),
);
// Root TXT: verification method and relationships
let root_txt = format!("vm=k0;auth=0;asm=0;inv=0;del=0");
packet.answers.push(ResourceRecord::new(
did_name.clone(),
CLASS::IN,
7200,
RData::TXT(simple_dns::rdata::TXT::new().with_string(&root_txt)?),
));
// Key 0: Ed25519 verification key
let key_name = Name::new_unchecked("_k0._did.");
let key_txt = format!("id=0;t=0;k={}", pubkey_b64);
packet.answers.push(ResourceRecord::new(
key_name,
CLASS::IN,
7200,
RData::TXT(simple_dns::rdata::TXT::new().with_string(&key_txt)?),
));
// Service endpoints
for (i, (id, endpoint)) in services.iter().enumerate() {
let svc_name = Name::new_unchecked(&format!("_s{}._did.", i));
let svc_txt = format!("id={};t=LinkedDomains;se={}", id, endpoint);
packet.answers.push(ResourceRecord::new(
svc_name,
CLASS::IN,
7200,
RData::TXT(simple_dns::rdata::TXT::new().with_string(&svc_txt)?),
));
}
Ok(packet.build_bytes_vec()?)
}
/// Parse a DNS packet back into a DID Document.
fn decode_dns_to_did_document(did: &str, dns_bytes: &[u8]) -> Result<serde_json::Value> {
use simple_dns::Packet;
let packet = Packet::parse(dns_bytes).context("Failed to parse DNS packet")?;
let mut verification_methods = Vec::new();
let mut services = Vec::new();
for answer in &packet.answers {
if let simple_dns::rdata::RData::TXT(txt) = answer.rdata.clone() {
let name = answer.name.to_string();
let text = txt.attributes().into_iter()
.map(|(k, v)| format!("{}={}", k, v.unwrap_or_default()))
.collect::<Vec<_>>()
.join(";");
if name.starts_with("_k") && name.contains("._did") {
// Parse key record
let attrs: HashMap<&str, &str> = text
.split(';')
.filter_map(|p| p.split_once('='))
.collect();
if let (Some(id), Some(key_type), Some(key_b64)) =
(attrs.get("id"), attrs.get("t"), attrs.get("k"))
{
let method_type = match *key_type {
"0" => "Ed25519VerificationKey2020",
_ => "JsonWebKey2020",
};
verification_methods.push(serde_json::json!({
"id": format!("{}#key-{}", did, id),
"type": method_type,
"controller": did,
"publicKeyMultibase": format!("z{}", key_b64),
}));
}
} else if name.starts_with("_s") && name.contains("._did") {
// Parse service record
let attrs: HashMap<&str, &str> = text
.split(';')
.filter_map(|p| p.split_once('='))
.collect();
if let (Some(id), Some(svc_type), Some(endpoint)) =
(attrs.get("id"), attrs.get("t"), attrs.get("se"))
{
services.push(serde_json::json!({
"id": format!("{}#{}", did, id),
"type": svc_type,
"serviceEndpoint": endpoint,
}));
}
}
}
}
let mut doc = serde_json::json!({
serde_json::json!({
"@context": [
"https://www.w3.org/ns/did/v1",
"https://w3id.org/security/suites/ed2020/v1"
],
"id": did,
"verificationMethod": verification_methods,
"authentication": verification_methods.iter()
.map(|vm| vm["id"].as_str().unwrap_or_default().to_string())
.collect::<Vec<_>>(),
"assertionMethod": verification_methods.iter()
.map(|vm| vm["id"].as_str().unwrap_or_default().to_string())
.collect::<Vec<_>>(),
});
"verificationMethod": [{
"id": format!("{}#key-0", did),
"type": "Ed25519VerificationKey2020",
"controller": did,
"publicKeyMultibase": format!("z{}", pubkey_b64),
}],
"authentication": [format!("{}#key-0", did)],
"assertionMethod": [format!("{}#key-0", did)],
"capabilityInvocation": [format!("{}#key-0", did)],
"capabilityDelegation": [format!("{}#key-0", did)],
})
}
if !services.is_empty() {
doc["service"] = serde_json::json!(services);
}
Ok(doc)
/// Encode the DID Document as bytes for DHT storage.
fn encode_for_dht(did_doc: &serde_json::Value) -> Vec<u8> {
serde_json::to_vec(did_doc).unwrap_or_default()
}
/// Create and publish a did:dht to the Mainline DHT.
/// Returns the did:dht identifier.
pub async fn create_and_publish(
signing_key: &SigningKey,
services: &[(&str, &str)],
_services: &[(&str, &str)],
) -> Result<String> {
let pubkey = signing_key.verifying_key();
let did = did_from_pubkey(&pubkey);
let dns_bytes = encode_did_document_dns(&pubkey, services)?;
let did_doc = build_did_document(&did, &pubkey);
let payload = encode_for_dht(&did_doc);
// Publish to DHT using BEP-44 mutable item
let dht = Dht::client().context("Failed to create DHT client")?;
let dht = mainline::Dht::client().context("Failed to create DHT client")?;
// Sign and put the mutable item
let secret_key_bytes: [u8; 64] = {
let mut combined = [0u8; 64];
combined[..32].copy_from_slice(&signing_key.to_bytes());
combined[32..].copy_from_slice(pubkey.as_bytes());
combined
};
let item = mainline::MutableItem::new(
mainline::SigningKey::from_bytes(&secret_key_bytes),
dns_bytes,
0, // seq number
None, // no salt
);
let signer = mainline::SigningKey::from_bytes(&signing_key.to_bytes());
let item = mainline::MutableItem::new(signer, payload, 0, None);
dht.put_mutable(item).context("Failed to publish to DHT")?;
@@ -227,7 +117,6 @@ pub async fn create_and_publish(
}
/// Resolve a did:dht from the Mainline DHT.
/// Returns the W3C DID Document.
pub async fn resolve(did: &str, cache: Option<&DhtDidCache>) -> Result<serde_json::Value> {
// Check cache first
if let Some(cache) = cache {
@@ -238,20 +127,21 @@ pub async fn resolve(did: &str, cache: Option<&DhtDidCache>) -> Result<serde_jso
}
let pubkey_bytes = pubkey_from_did(did)?;
let pubkey = VerifyingKey::from_bytes(&pubkey_bytes)
.context("Invalid Ed25519 public key in did:dht")?;
let verifying_key = mainline::VerifyingKey::from_bytes(&pubkey_bytes)
.map_err(|e| anyhow::anyhow!("Invalid Ed25519 key: {:?}", e))?;
let target = mainline::MutableItem::target_from_key(&verifying_key, &None);
let dht = Dht::client().context("Failed to create DHT client")?;
// Get the mutable item from DHT
let target = mainline::MutableItem::target_from_key(
&mainline::VerifyingKey::from_bytes(&pubkey_bytes).context("Invalid key")?,
&None,
);
let dht = mainline::Dht::client().context("Failed to create DHT client")?;
let response = tokio::time::timeout(
std::time::Duration::from_secs(30),
tokio::task::spawn_blocking(move || dht.get_mutable(&target, None, None)),
tokio::task::spawn_blocking(move || {
// get_mutable returns a Result<IntoIter<MutableItem>>
match dht.get_mutable(&target, None, None) {
Ok(mut iter) => iter.next(),
Err(_) => None,
}
}),
)
.await
.context("DHT resolution timed out")?
@@ -259,10 +149,9 @@ pub async fn resolve(did: &str, cache: Option<&DhtDidCache>) -> Result<serde_jso
match response {
Some(item) => {
let dns_bytes = item.value();
let doc = decode_dns_to_did_document(did, dns_bytes)?;
let doc: serde_json::Value = serde_json::from_slice(item.value())
.context("Failed to parse DID Document from DHT")?;
// Cache the result
if let Some(cache) = cache {
cache.set(did.to_string(), doc.clone()).await;
}
@@ -278,7 +167,9 @@ pub async fn resolve(did: &str, cache: Option<&DhtDidCache>) -> Result<serde_jso
/// Store the did:dht identifier for an identity record.
pub async fn save_dht_did(data_dir: &Path, identity_id: &str, dht_did: &str) -> Result<()> {
let path = data_dir.join("identities").join(format!("{}.json", identity_id));
let path = data_dir
.join("identities")
.join(format!("{}.json", identity_id));
if !path.exists() {
anyhow::bail!("Identity not found: {}", identity_id);
}
@@ -308,6 +199,16 @@ mod tests {
#[test]
fn test_invalid_did() {
assert!(pubkey_from_did("did:key:z123").is_err());
assert!(pubkey_from_did("did:dht:").is_err());
}
#[test]
fn test_build_did_document() {
let key = SigningKey::generate(&mut rand::rngs::OsRng);
let pubkey = key.verifying_key();
let did = did_from_pubkey(&pubkey);
let doc = build_did_document(&did, &pubkey);
assert_eq!(doc["id"], did);
assert!(doc["verificationMethod"].as_array().unwrap().len() > 0);
}
}