Skip to main content

kya_validator/
resolver.rs

1use crate::types::{DidDocument, KeyType, Manifest, ResolvedKey, VerificationMethod};
2use std::collections::HashMap;
3
4#[cfg(not(target_arch = "wasm32"))]
5use reqwest::blocking::Client;
6
7pub(crate) fn resolve_verification_methods(
8    manifest: &Manifest,
9) -> HashMap<String, VerificationMethod> {
10    let mut methods = HashMap::new();
11    if let Some(list) = manifest.verification_method.as_ref() {
12        for method in list {
13            methods.insert(method.id.clone(), method.clone());
14        }
15    }
16    methods
17}
18
19pub fn decode_multibase(input: &str) -> Result<Vec<u8>, String> {
20    let mut chars = input.chars();
21    let prefix = chars
22        .next()
23        .ok_or_else(|| "Empty multibase value".to_string())?;
24    let payload: String = chars.collect();
25    match prefix {
26        'z' => bs58::decode(payload)
27            .into_vec()
28            .map_err(|err| format!("Base58 decode failed: {}", err)),
29        _ => Err(format!("Unsupported multibase prefix: {}", prefix)),
30    }
31}
32
33fn read_varint(bytes: &[u8]) -> Result<(u64, usize), String> {
34    let mut result = 0u64;
35    let mut shift = 0u32;
36    for (index, byte) in bytes.iter().enumerate() {
37        let value = (byte & 0x7F) as u64;
38        result |= value << shift;
39        if (byte & 0x80) == 0 {
40            return Ok((result, index + 1));
41        }
42        shift += 7;
43    }
44    Err("Invalid varint".to_string())
45}
46
47fn key_type_from_method(method_type: &str) -> Result<KeyType, String> {
48    match method_type {
49        "Ed25519VerificationKey2020" => Ok(KeyType::Ed25519),
50        "EcdsaSecp256k1VerificationKey2019" => Ok(KeyType::Secp256k1),
51        other => Err(format!("Unsupported key type: {}", other)),
52    }
53}
54
55fn key_type_from_multicodec(code: u64) -> Result<KeyType, String> {
56    match code {
57        0xed => Ok(KeyType::Ed25519),
58        0xe7 => Ok(KeyType::Secp256k1),
59        other => Err(format!("Unsupported multicodec: {}", other)),
60    }
61}
62
63#[cfg(not(target_arch = "wasm32"))]
64pub(crate) fn resolve_did_web(did: &str) -> Result<DidDocument, String> {
65    let did_parts = did
66        .strip_prefix("did:web:")
67        .ok_or_else(|| "Invalid did:web format".to_string())?;
68    let parts: Vec<&str> = did_parts.split(':').collect();
69    let domain = parts
70        .first()
71        .ok_or_else(|| "Missing did:web domain".to_string())?;
72    let path = if parts.len() > 1 {
73        format!("/{}/did.json", parts[1..].join("/"))
74    } else {
75        "/.well-known/did.json".to_string()
76    };
77    let url = format!("https://{}{}", domain, path);
78
79    let client = Client::new();
80    let response = client
81        .get(&url)
82        .send()
83        .map_err(|err| format!("Failed to fetch DID document: {}", err))?;
84    response
85        .json::<DidDocument>()
86        .map_err(|err| format!("Failed to parse DID document: {}", err))
87}
88
89#[cfg(target_arch = "wasm32")]
90pub(crate) fn resolve_did_web(_did: &str) -> Result<DidDocument, String> {
91    Err("DID web resolution not supported on WASM - use browser fetch".to_string())
92}
93
94pub(crate) fn resolve_did_key(did: &str) -> Result<ResolvedKey, String> {
95    let encoded = did
96        .strip_prefix("did:key:")
97        .ok_or_else(|| "Invalid did:key format".to_string())?;
98    let decoded = decode_multibase(encoded)?;
99    let (codec, offset) = read_varint(&decoded)?;
100    let key_type = key_type_from_multicodec(codec)?;
101    let key_bytes = decoded
102        .get(offset..)
103        .ok_or_else(|| "Missing key material".to_string())?
104        .to_vec();
105    let method_id = format!("{}#key-1", did);
106    Ok(ResolvedKey {
107        id: method_id,
108        controller: did.to_string(),
109        key_type,
110        public_key: key_bytes,
111    })
112}
113
114pub(crate) fn resolve_key_from_method(method: &VerificationMethod) -> Result<ResolvedKey, String> {
115    let key_type = key_type_from_method(&method.method_type)?;
116    let public_key_multibase = method
117        .public_key_multibase
118        .as_ref()
119        .ok_or_else(|| "Missing publicKeyMultibase".to_string())?;
120    let key_bytes = decode_multibase(public_key_multibase)?;
121    Ok(ResolvedKey {
122        id: method.id.clone(),
123        controller: method.controller.clone(),
124        key_type,
125        public_key: key_bytes,
126    })
127}
128
129/// Blockchain network identifier for did:pkh
130#[derive(Debug, Clone, PartialEq, Eq)]
131pub enum BlockchainNetwork {
132    Ethereum,
133    Polygon,
134    Bsc,
135    Arbitrum,
136    Optimism,
137    Avalanche,
138    Other(String),
139}
140
141/// Parsed did:pkh information
142#[derive(Debug, Clone)]
143pub struct DidPkhInfo {
144    pub network: BlockchainNetwork,
145    pub address: String,
146    pub chain_id: Option<u64>,
147}
148
149/// Parse did:pkh to extract blockchain address
150/// Format: did:pkh:eip155:chainId:address
151pub fn parse_did_pkh(did: &str) -> Result<DidPkhInfo, String> {
152    let parts: Vec<&str> = did.split(':').collect();
153
154    if parts.len() < 4 {
155        return Err(format!("Invalid did:pkh format: {}", did));
156    }
157
158    // did:pkh is two parts: "did" and "pkh"
159    let did_method = format!("{}:{}", parts[0], parts[1]);
160    if did_method != "did:pkh" {
161        return Err(format!("Expected did:pkh, got: {}", did_method));
162    }
163
164    // After "did:pkh", the next parts are: method, chainId, address
165    if parts.len() < 5 {
166        return Err(format!(
167            "Invalid did:pkh format: expected 5 parts, got {}",
168            parts.len()
169        ));
170    }
171
172    let method = parts[2];
173    if method != "eip155" {
174        return Err(format!("Unsupported did:pkh method: {}", method));
175    }
176
177    let chain_id: u64 = parts[3]
178        .parse()
179        .map_err(|e| format!("Invalid chain ID: {}", e))?;
180
181    let address = parts[4];
182
183    // Validate Ethereum address format
184    if address.len() != 42 || !address.starts_with("0x") {
185        return Err("Invalid Ethereum address format".to_string());
186    }
187
188    let network = match chain_id {
189        1 => BlockchainNetwork::Ethereum,
190        137 => BlockchainNetwork::Polygon,
191        56 => BlockchainNetwork::Bsc,
192        42161 => BlockchainNetwork::Arbitrum,
193        10 => BlockchainNetwork::Optimism,
194        43114 => BlockchainNetwork::Avalanche,
195        _ => BlockchainNetwork::Other(format!("chain-{}", chain_id)),
196    };
197
198    Ok(DidPkhInfo {
199        network,
200        address: address.to_lowercase(),
201        chain_id: Some(chain_id),
202    })
203}
204
205pub(crate) fn resolve_key_for_method(
206    method_id: &str,
207    manifest: &Manifest,
208) -> Result<ResolvedKey, String> {
209    let methods = resolve_verification_methods(manifest);
210    if let Some(method) = methods.get(method_id) {
211        return resolve_key_from_method(method);
212    }
213
214    let did = method_id
215        .split('#')
216        .next()
217        .ok_or_else(|| "Invalid verification method".to_string())?;
218
219    if did.starts_with("did:key:") {
220        let mut resolved = resolve_did_key(did)?;
221        resolved.id = method_id.to_string();
222        return Ok(resolved);
223    }
224
225    if did.starts_with("did:web:") {
226        let document = resolve_did_web(did)?;
227        if let Some(methods) = document.verification_method {
228            if let Some(method) = methods.iter().find(|entry| entry.id == method_id) {
229                return resolve_key_from_method(method);
230            }
231            return Err(format!("Verification method {} not found", method_id));
232        }
233        return Err(format!("No verification methods for {}", did));
234    }
235
236    if did.starts_with("did:pkh:") {
237        let _pkh_info = parse_did_pkh(did)?;
238        return Err(format!(
239            "did:pkh is for solvency links, not cryptographic verification. \
240            Network: {:?}, Address: {}",
241            _pkh_info.network, _pkh_info.address
242        ));
243    }
244
245    Err(format!("Unsupported DID method in {}", method_id))
246}
247
248#[cfg(test)]
249mod tests {
250    use super::*;
251
252    #[test]
253    fn resolve_did_key_decodes_multibase() {
254        let bytes = vec![0xed, 0x01, 0x02, 0x03, 0x04];
255        let encoded = format!("z{}", bs58::encode(&bytes).into_string());
256        let did = format!("did:key:{}", encoded);
257        let resolved = resolve_did_key(&did).expect("should resolve did:key");
258        assert_eq!(resolved.key_type, KeyType::Ed25519);
259        assert_eq!(resolved.public_key, vec![0x02, 0x03, 0x04]);
260        assert_eq!(resolved.controller, did);
261    }
262}