Skip to main content

qualia_core_db/sparql_library/
sparql_did.rs

1//! SPARQL-DID Integration
2//!
3//! Zero-allocation implementation of DID extension functions for SPARQL.
4//! Implements Appendix B of the SPARQL-DID Integration Specification.
5
6use crate::sparql_ast::*;
7use crate::NQuin;
8
9/// DID resolution result for the SPARQL ABI layer.
10///
11/// This is the **pointer-resolution** view: a `did:q42` URI is a topological
12/// coordinate (see [`crate::identifier::parse_did_q42`]), and the SPARQL query
13/// engine addresses DIDs as `u64` pointers. `endpoint_url` and
14/// `verification_method` here are therefore `q_hash`-space pointers to the
15/// human-readable strings produced by [`DIDResolver::resolve`].
16///
17/// Fixed-size (`#[repr(C)]`) to preserve the zero-allocation ABI used by the
18/// SPARQL `did:resolve` magic-property wrapper.
19#[repr(C)]
20#[derive(Debug, Clone, Copy)]
21pub struct DidResolutionPointer {
22    pub did: u64,
23    pub endpoint_url: u64,
24    pub verification_method: u64,
25    pub expires: u64,
26}
27
28/// DID resolution result (human-readable, string-based).
29///
30/// Produced by [`DIDResolver::resolve`]. This is **not** a hot path (the SPARQL
31/// ABI layer uses [`DidResolutionPointer`]), so it owns `String`s and carries
32/// the real, deterministic endpoint URL and verification method for a DID.
33#[derive(Debug, Clone)]
34pub struct DidResolutionResult {
35    /// The DID that was resolved (e.g. `did:q42:z6MkpTHR8VNs`).
36    pub did: String,
37    /// A valid, deterministic HTTPS endpoint URL for the DID method.
38    pub endpoint_url: String,
39    /// The verification method identifier (a DID URI fragment or absolute URI).
40    pub verification_method: String,
41    /// Unix-epoch milliseconds at which the resolution was performed.
42    pub resolved_at: u64,
43}
44
45/// DID signature verification result
46#[repr(C)]
47#[derive(Debug, Clone, Copy)]
48pub struct DidVerificationResult {
49    pub did: u64,
50    pub valid: bool,
51    pub algorithm: u8,
52}
53
54/// DID permission check result
55#[repr(C)]
56#[derive(Debug, Clone, Copy)]
57pub struct DidPermissionResult {
58    pub did: u64,
59    pub graph: u64,
60    pub has_permission: bool,
61    pub permission_type: u8, // 0=read, 1=write, 2=admin
62}
63
64/// DID cache entry (fixed-size array for zero-allocation)
65#[repr(C)]
66#[derive(Debug, Clone, Copy)]
67pub struct DidCacheEntry {
68    pub did: u64,
69    pub resolution: DidResolutionPointer,
70    pub timestamp: u64,
71    pub ttl: u32,
72}
73
74/// SPARQL-DID Handler
75pub struct SparqlDidHandler<'a> {
76    pub quins: &'a [NQuin],
77    pub did_cache: [Option<DidCacheEntry>; 32],
78    pub cache_count: u8,
79}
80
81impl<'a> SparqlDidHandler<'a> {
82    pub fn new(quins: &'a [NQuin]) -> Self {
83        Self {
84            quins,
85            did_cache: [None; 32],
86            cache_count: 0,
87        }
88    }
89
90    /// Resolve a `u64` DID pointer to ABI-layer endpoint/verification pointers
91    /// (with caching).
92    ///
93    /// `endpoint_url` and `verification_method` are derived via FNV-1a over the
94    /// DID's 60-bit identity bytes, salted with a method tag — **not** XOR with a
95    /// magic number. They live in the same `q_hash` pointer space as the rest of
96    /// the SPARQL engine. The human-readable URL and verification-method string
97    /// for a DID are available via [`DIDResolver::resolve`].
98    pub fn resolve_did(&mut self, did: u64) -> Result<DidResolutionPointer, String> {
99        // Check cache first (zero-allocation lookup)
100        for i in 0..self.cache_count as usize {
101            if let Some(entry) = self.did_cache[i] {
102                if entry.did == did {
103                    let now = self.current_timestamp();
104                    // `ttl` is in seconds; timestamps are ms. Saturating so a clock skew
105                    // (now < timestamp) simply treats the entry as fresh, never panics.
106                    if now.saturating_sub(entry.timestamp) < (entry.ttl as u64) * 1000 {
107                        return Ok(entry.resolution);
108                    }
109                }
110            }
111        }
112
113        // Deterministic pointer derivation: FNV-1a over the 60-bit identity,
114        // salted with a domain tag so endpoint and verification-method pointers
115        // cannot collide with each other or with plain dictionary hashes.
116        let id_bytes = (did & 0x0FFF_FFFF_FFFF_FFFF).to_le_bytes();
117        let endpoint_url = fnv1a_tagged(b"did:endpoint:", &id_bytes);
118        let verification_method = fnv1a_tagged(b"did:vm:", &id_bytes);
119
120        let resolution = DidResolutionPointer {
121            did,
122            endpoint_url,
123            verification_method,
124            expires: self.current_timestamp() + 3600000, // 1 hour TTL
125        };
126
127        // Cache the result
128        if self.cache_count < 32 {
129            let entry = DidCacheEntry {
130                did,
131                resolution,
132                timestamp: self.current_timestamp(),
133                ttl: 3600,
134            };
135            self.did_cache[self.cache_count as usize] = Some(entry);
136            self.cache_count += 1;
137        }
138
139        Ok(resolution)
140    }
141
142    /// Verify DID signature (zero-allocation using stack-allocated key frame)
143    pub fn verify_signature(
144        &self,
145        _did: u64,
146        signature: &[u8],
147        data: &[u8],
148    ) -> Result<DidVerificationResult, String> {
149        if signature.len() != 64 {
150            return Err("Invalid signature length".to_string());
151        }
152
153        let _ = (signature, data);
154
155        #[cfg(feature = "interop-crypto")]
156        {
157            use ed25519_dalek::{Signature, Verifier, VerifyingKey};
158
159            let mut sig_bytes = [0u8; 64];
160            sig_bytes.copy_from_slice(signature);
161            if let Ok(sig) = Signature::from_bytes(&sig_bytes) {
162                // Fast-path: if the SPARQL query supplies the public key prepended to the data
163                // (32 bytes PK + payload), we can verify it immediately at the boundary.
164                if data.len() > 32 {
165                    let mut pk_bytes = [0u8; 32];
166                    pk_bytes.copy_from_slice(&data[0..32]);
167                    if let Ok(verifying_key) = VerifyingKey::from_bytes(&pk_bytes) {
168                        let valid = verifying_key.verify(&data[32..], &sig).is_ok();
169                        if valid {
170                            return Ok(DidVerificationResult {
171                                did: _did,
172                                valid: true,
173                                algorithm: 1, // Ed25519
174                            });
175                        }
176                    }
177                }
178            }
179        }
180
181        // If we reach here, either interop-crypto is disabled or the fast-path failed.
182        // We do not have the public key locally, so we fail closed.
183        Err(
184            "did:verify fast-path failed: no resolvable verification key is provisioned \
185             in the SPARQL read-side shim. Verify via the identity/key-vault layer \
186             (KeyVault::verify_signature)."
187                .to_string(),
188        )
189    }
190
191    /// Check DID permission for graph access
192    pub fn check_permission(
193        &self,
194        did: u64,
195        graph: u64,
196        permission_type: u8,
197    ) -> Result<DidPermissionResult, String> {
198        // Check DID has 0x8 prefix
199        if (did & 0x8000000000000000) == 0 {
200            return Err("Invalid DID: missing 0x8 prefix".to_string());
201        }
202
203        let _ = (graph, permission_type);
204
205        // FAIL CLOSED. Access control must be decided by an authority that has
206        // actually evaluated the permission graph (the Webizen VM / deontic policy
207        // layer), not granted unconditionally here. Returning `has_permission: true`
208        // would be an authorization bypass: any caller would be granted any graph.
209        Err(
210            "did:permission is not available in the SPARQL query layer: \
211             access-control decisions must be evaluated against the policy graph by \
212             the governance layer, not granted unconditionally here."
213                .to_string(),
214        )
215    }
216
217    /// Authenticate with DID (strips heavy payloads at boundary)
218    pub fn authenticate_did(
219        &self,
220        did: u64,
221        auth_method: u8,
222        _auth_payload: &[u8],
223    ) -> Result<bool, String> {
224        // Check DID has 0x8 prefix
225        if (did & 0x8000000000000000) == 0 {
226            return Err("Invalid DID: missing 0x8 prefix".to_string());
227        }
228
229        let _ = auth_method;
230
231        // FAIL CLOSED. Authentication requires verifying `_auth_payload` (a JSON-LD
232        // proof / VC / challenge response) against a resolved verification method.
233        // This shim strips that payload at the boundary and holds no key material, so
234        // it cannot authenticate anyone. Returning `Ok(true)` would authenticate every
235        // caller. Route authentication through the identity layer instead.
236        Err("did:auth is not available in the SPARQL query layer: \
237             the authentication proof is stripped at this boundary and cannot be \
238             verified here. Authenticate via the identity/key-vault layer."
239            .to_string())
240    }
241
242    /// Current wall-clock time in Unix-epoch milliseconds (real system time). Used for
243    /// cache freshness; `0` only if the system clock is before the epoch.
244    fn current_timestamp(&self) -> u64 {
245        std::time::SystemTime::now()
246            .duration_since(std::time::UNIX_EPOCH)
247            .map(|d| d.as_millis() as u64)
248            .unwrap_or(0)
249    }
250
251    /// Sign data with DID (zero-allocation)
252    pub fn sign_with_did(&self, did: u64, data: &[u8]) -> Result<Vec<u8>, String> {
253        // Check DID has 0x8 prefix
254        if (did & 0x8000000000000000) == 0 {
255            return Err("Invalid DID: missing 0x8 prefix".to_string());
256        }
257
258        // The SPARQL-DID handler is a read-side query shim: it resolves DIDs to u64
259        // pointers and deliberately strips heavy crypto payloads at the boundary
260        // (see `authenticate_did`). It holds NO private key material, so it cannot and
261        // must not produce a signature here. Signing is the responsibility of the
262        // identity / key-vault layer (e.g. `WebizenIdentityManager` over `key_vault`,
263        // or `CryptographicLibrary::sign_data`), which owns the secret keys.
264        //
265        // Fail closed rather than returning a forged all-zero signature that would
266        // falsely signal success to callers.
267        let _ = data;
268        Err("did:sign is not available in the SPARQL query layer: \
269             no private key is provisioned here. Sign via the identity/key-vault \
270             layer (WebizenIdentityManager / CryptographicLibrary::sign_data)."
271            .to_string())
272    }
273
274    /// Invalidate cache entry
275    pub fn invalidate_cache(&mut self, did: u64) {
276        for i in 0..self.cache_count as usize {
277            if let Some(entry) = self.did_cache[i] {
278                if entry.did == did {
279                    self.did_cache[i] = None;
280                    // Compact cache
281                    for j in i..self.cache_count as usize - 1 {
282                        self.did_cache[j] = self.did_cache[j + 1];
283                    }
284                    self.cache_count -= 1;
285                    return;
286                }
287            }
288        }
289    }
290}
291
292impl<'a> Default for SparqlDidHandler<'a> {
293    fn default() -> Self {
294        Self::new(&[])
295    }
296}
297
298/// DID extension functions (Appendix B)
299/// These are assigned to 0x0 standard dictionary type prefix during planning
300/// to identify them as Magic Property Functions
301
302/// did:resolve - Resolve DID to Document
303///
304/// ABI-layer wrapper: emits the `endpoint_url` and `verification_method`
305/// **pointers** (in `q_hash` space) for the SPARQL binding. For the
306/// human-readable URL, use [`DIDResolver::resolve`].
307pub fn did_resolve(args: &[u64], quins: &[NQuin], result: &mut BindingRow) -> bool {
308    if args.is_empty() {
309        return false;
310    }
311    let did = args[0];
312
313    let mut handler = SparqlDidHandler::new(quins);
314    match handler.resolve_did(did) {
315        Ok(resolution) => {
316            result.slots[0] = Some(resolution.endpoint_url);
317            result.slots[1] = Some(resolution.verification_method);
318            true
319        }
320        Err(_) => false,
321    }
322}
323
324/// did:verify - Verify DID signature
325pub fn did_verify(args: &[u64], quins: &[NQuin], result: &mut BindingRow) -> bool {
326    if args.len() < 3 {
327        return false;
328    }
329    let did = args[0];
330    let signature_ptr = args[1];
331    let data_ptr = args[2];
332
333    let handler = SparqlDidHandler::new(quins);
334    // SAFETY: never interpret a query-supplied `u64` as a raw memory pointer — that was
335    // undefined behaviour / an arbitrary-read hazard, and violates "no native pointers
336    // cross the query API". This magic-predicate wrapper holds no resolver to recover the
337    // real signature/data bytes, so it passes empty slices and `verify_signature` fails
338    // closed. Real byte payloads reach did:verify through the resolver-aware dispatch.
339    let _ = (signature_ptr, data_ptr);
340    let signature: &[u8] = &[];
341    let data: &[u8] = &[];
342
343    match handler.verify_signature(did, signature, data) {
344        Ok(verification) => {
345            result.slots[0] = Some(if verification.valid { 1 } else { 0 });
346            result.slots[1] = Some(verification.algorithm as u64);
347            true
348        }
349        Err(_) => false,
350    }
351}
352
353/// did:auth - Authenticate with DID
354pub fn did_auth(args: &[u64], quins: &[NQuin], result: &mut BindingRow) -> bool {
355    if args.len() < 2 {
356        return false;
357    }
358    let did = args[0];
359    let auth_method = args[1] as u8;
360
361    let handler = SparqlDidHandler::new(quins);
362    // SAFETY: do NOT dereference a query-supplied `u64` as a pointer (see did_verify).
363    // The proof payload is not recoverable here; pass empty → authenticate_did fails
364    // closed (it already refuses to authenticate without a verifiable proof).
365    let auth_payload: &[u8] = &[];
366
367    match handler.authenticate_did(did, auth_method, auth_payload) {
368        Ok(valid) => {
369            result.slots[0] = Some(if valid { 1 } else { 0 });
370            true
371        }
372        Err(_) => false,
373    }
374}
375
376/// did:sign - Sign with DID
377pub fn did_sign(args: &[u64], quins: &[NQuin], result: &mut BindingRow) -> bool {
378    if args.len() < 2 {
379        return false;
380    }
381    let did = args[0];
382    let data_ptr = args[1];
383
384    let handler = SparqlDidHandler::new(quins);
385    // SAFETY: do NOT dereference a query-supplied `u64` as a pointer (see did_verify).
386    // sign_with_did fails closed regardless (no private key in the query layer).
387    let _ = data_ptr;
388    let data: &[u8] = &[];
389
390    match handler.sign_with_did(did, data) {
391        Ok(_signature) => {
392            result.slots[0] = Some(1); // Success indicator
393            true
394        }
395        Err(_) => false,
396    }
397}
398
399/// did:permission - Check DID permission
400pub fn did_permission(args: &[u64], quins: &[NQuin], result: &mut BindingRow) -> bool {
401    if args.len() < 3 {
402        return false;
403    }
404    let did = args[0];
405    let graph = args[1];
406    let permission_type = args[2] as u8;
407
408    let handler = SparqlDidHandler::new(quins);
409    match handler.check_permission(did, graph, permission_type) {
410        Ok(permission) => {
411            result.slots[0] = Some(if permission.has_permission { 1 } else { 0 });
412            result.slots[1] = Some(permission.permission_type as u64);
413            true
414        }
415        Err(_) => false,
416    }
417}
418
419/// FNV-1a over a domain tag followed by a byte payload, truncated to 60 bits.
420///
421/// Used by [`SparqlDidHandler::resolve_did`] to derive deterministic
422/// `endpoint_url` / `verification_method` pointers from a DID's identity bits
423/// without XOR-ing against a magic constant. Shares the same FNV constants and
424/// 60-bit mask as [`crate::q_hash`] / [`crate::identifier::parse_did_q42`].
425#[inline]
426fn fnv1a_tagged(tag: &[u8], payload: &[u8]) -> u64 {
427    let mut hash: u64 = 0xcbf29ce484222325;
428    for &b in tag {
429        hash ^= b as u64;
430        hash = hash.wrapping_mul(0x100000001b3);
431    }
432    for &b in payload {
433        hash ^= b as u64;
434        hash = hash.wrapping_mul(0x100000001b3);
435    }
436    hash & 0x0FFF_FFFF_FFFF_FFFF
437}
438
439/// Human-readable DID resolver.
440///
441/// Maps a DID string to a deterministic, valid HTTPS endpoint URL and
442/// verification-method identifier according to the DID method's resolution
443/// convention. This is **not** HTTP resolution (no network call is made); it
444/// produces the URL that *would* be fetched by a universal resolver / the
445/// method's native resolution endpoint. This module is not a hot path, so it
446/// owns `String`s freely.
447///
448/// # Supported methods
449/// | Method | Endpoint URL | Verification method |
450/// |--------|--------------|---------------------|
451/// | `did:q42:` | `https://q42.network/agents/{id_hex}` | `{did}#q42-key` |
452/// | `did:web:` | `https://{domain}/.well-known/did.json` | `{did}#key-1` |
453/// | `did:key:` | `https://dev.uniresolver.io/1.0/identifiers/{did}` | `{did}` |
454pub struct DIDResolver;
455
456impl DIDResolver {
457    /// Construct a new resolver.
458    pub fn new() -> Self {
459        Self
460    }
461
462    /// Resolve a DID to a human-readable endpoint URL and verification method.
463    ///
464    /// Delegates to [`Self::resolve_did`]; provided as the canonical entry point
465    /// named in the SPARQL-DID Integration Specification.
466    pub fn resolve(&self, did: &str) -> Result<DidResolutionResult, String> {
467        self.resolve_did(did)
468    }
469
470    /// Resolve a DID to a human-readable endpoint URL and verification method.
471    pub fn resolve_did(&self, did: &str) -> Result<DidResolutionResult, String> {
472        let resolved_at = current_epoch_millis();
473
474        if let Some(rest) = did.strip_prefix("did:q42:") {
475            if rest.is_empty() {
476                return Err("did:q42 resolution failed: empty identifier".to_string());
477            }
478            // Parse via the identifier module to validate the DID and obtain the
479            // canonical 60-bit topological pointer; the low 60 bits are the
480            // identity hash, which we render as hex for the agent URL.
481            let pointer = crate::identifier::parse_did_q42(did.as_bytes())
482                .map_err(|e| format!("did:q42 resolution failed: {:?}", e))?;
483            let id_hex = format!("{:015x}", pointer & 0x0FFF_FFFF_FFFF_FFFF);
484            Ok(DidResolutionResult {
485                did: did.to_string(),
486                endpoint_url: format!("https://q42.network/agents/{}", id_hex),
487                verification_method: format!("{}#q42-key", did),
488                resolved_at,
489            })
490        } else if let Some(rest) = did.strip_prefix("did:web:") {
491            if rest.is_empty() {
492                return Err("did:web resolution failed: empty domain".to_string());
493            }
494            // did:web resolution: replace ':' with '/' for path components, then
495            // append the standard well-known DID document location.
496            let domain_path = rest.replace(':', "/");
497            Ok(DidResolutionResult {
498                did: did.to_string(),
499                endpoint_url: format!("https://{}/.well-known/did.json", domain_path),
500                verification_method: format!("{}#key-1", did),
501                resolved_at,
502            })
503        } else if let Some(rest) = did.strip_prefix("did:key:") {
504            if rest.is_empty() {
505                return Err("did:key resolution failed: empty key".to_string());
506            }
507            // did:key embeds the public key in the DID itself; the DID is its own
508            // verification method (per the did:key spec). There is no native HTTP
509            // endpoint, so we point at a universal resolver for retrieval.
510            Ok(DidResolutionResult {
511                did: did.to_string(),
512                endpoint_url: format!("https://dev.uniresolver.io/1.0/identifiers/{}", did),
513                verification_method: did.to_string(),
514                resolved_at,
515            })
516        } else if did.starts_with("did:") {
517            // Unknown DID method.
518            let method = did
519                .get(4..)
520                .and_then(|s| s.split(':').next())
521                .unwrap_or("unknown");
522            Err(format!(
523                "DID resolution failed: unsupported DID method '{}'. \
524                 Supported methods: did:q42, did:web, did:key.",
525                method
526            ))
527        } else {
528            Err(format!(
529                "DID resolution failed: '{}' is not a valid DID (must start with 'did:')",
530                did
531            ))
532        }
533    }
534
535    /// Verify a DID's authentication signature.
536    ///
537    /// This performs only the **structural** checks available to the read-side
538    /// SPARQL query layer (that the DID is well-formed and resolvable, and that
539    /// a non-empty signature and payload were supplied). The actual
540    /// cryptographic verification requires public-key material that this shim
541    /// deliberately does not hold — it is the responsibility of the identity /
542    /// key-vault layer ([`crate::key_vault::KeyVault::verify_signature`] /
543    /// `WebizenIdentityRegistry::verify_signature`).
544    ///
545    /// Fails closed: never reports a signature as valid from this layer.
546    pub fn verify_authentication_signature(
547        &self,
548        did: &str,
549        signature: &[u8],
550        payload: &[u8],
551    ) -> Result<bool, String> {
552        // Structural validation: the DID must resolve (be well-formed + known method).
553        self.resolve_did(did)?;
554
555        if signature.is_empty() {
556            return Err("DID signature verification failed: empty signature".to_string());
557        }
558        if payload.is_empty() {
559            return Err("DID signature verification failed: empty payload".to_string());
560        }
561
562        // FAIL CLOSED — see the security rationale on
563        // [`SparqlDidHandler::verify_signature`]. This layer holds no
564        // verifiable public key, so it cannot perform a real Ed25519/ML-DSA
565        // check. Returning `true` would forge a positive verification.
566        Err("did:verify is not available in the SPARQL query layer: \
567             no resolvable verification key is provisioned here. Verify via the \
568             identity/key-vault layer (KeyVault::verify_signature / \
569             WebizenIdentityRegistry::verify_signature)."
570            .to_string())
571    }
572}
573
574impl Default for DIDResolver {
575    fn default() -> Self {
576        Self::new()
577    }
578}
579
580/// Current time as Unix-epoch milliseconds.
581///
582/// The SPARQL ABI layer (`SparqlDidHandler::current_timestamp`) returns a
583/// placeholder because it must stay allocation-free and deterministic for the
584/// query planner. The human-readable resolver is not a hot path, so it uses
585/// real wall-clock time.
586fn current_epoch_millis() -> u64 {
587    use std::time::{SystemTime, UNIX_EPOCH};
588    SystemTime::now()
589        .duration_since(UNIX_EPOCH)
590        .map(|d| d.as_millis() as u64)
591        .unwrap_or(0)
592}
593
594#[cfg(test)]
595mod tests {
596    use super::*;
597
598    #[test]
599    fn test_did_handler_creation() {
600        let quins = vec![];
601        let handler = SparqlDidHandler::new(&quins);
602        assert_eq!(handler.cache_count, 0);
603    }
604
605    #[test]
606    fn test_resolve_did() {
607        let quins = vec![];
608        let mut handler = SparqlDidHandler::new(&quins);
609
610        let result = handler.resolve_did(0x8000000000000001); // With 0x8 prefix
611        assert!(result.is_ok());
612        assert_eq!(result.unwrap().did, 0x8000000000000001);
613    }
614
615    #[test]
616    fn test_verify_signature_fails_closed() {
617        // Security regression: the SPARQL/DID query shim must NOT rubber-stamp
618        // signatures. It has no resolvable key here and must fail closed so callers
619        // route verification through the key-vault/identity layer.
620        let quins = vec![];
621        let handler = SparqlDidHandler::new(&quins);
622
623        let signature = &[0u8; 64];
624        let data = &[0u8; 256];
625
626        let result = handler.verify_signature(0x8000000000000001, signature, data);
627        assert!(
628            result.is_err(),
629            "verify_signature must fail closed, not return valid"
630        );
631    }
632
633    #[test]
634    fn test_check_permission_fails_closed() {
635        // Security regression: permission must not be granted unconditionally.
636        let quins = vec![];
637        let handler = SparqlDidHandler::new(&quins);
638
639        let result = handler.check_permission(0x8000000000000001, 123, 0);
640        assert!(
641            result.is_err(),
642            "check_permission must fail closed, not grant access"
643        );
644    }
645
646    #[test]
647    fn test_authenticate_did_fails_closed() {
648        // Security regression: authentication must not succeed for everyone.
649        let quins = vec![];
650        let handler = SparqlDidHandler::new(&quins);
651
652        let result = handler.authenticate_did(0x8000000000000001, 1, &[0u8; 256]);
653        assert!(
654            result.is_err(),
655            "authenticate_did must fail closed, not authenticate all"
656        );
657    }
658
659    // ===== DIDResolver (human-readable, string-based) tests =====
660
661    #[test]
662    fn test_resolver_did_q42_produces_valid_url() {
663        let resolver = DIDResolver::new();
664        let did = "did:q42:z6MkpTHR8VNs";
665        let result = resolver.resolve(did).expect("did:q42 must resolve");
666
667        assert_eq!(result.did, did);
668        // Endpoint must be a valid HTTPS URL — not XOR garbage.
669        assert!(
670            result
671                .endpoint_url
672                .starts_with("https://q42.network/agents/"),
673            "endpoint_url should be a q42.network agent URL, got '{}'",
674            result.endpoint_url
675        );
676        // The agent id must be a non-empty hex string (the 60-bit identity).
677        let agent_id = result
678            .endpoint_url
679            .strip_prefix("https://q42.network/agents/")
680            .unwrap();
681        assert!(!agent_id.is_empty(), "agent id must not be empty");
682        assert!(
683            agent_id.chars().all(|c| c.is_ascii_hexdigit()),
684            "agent id must be hex, got '{}'",
685            agent_id
686        );
687        assert_eq!(result.verification_method, format!("{}#q42-key", did));
688        assert!(result.resolved_at > 0, "resolved_at must be set");
689    }
690
691    #[test]
692    fn test_resolver_did_q42_is_deterministic() {
693        let resolver = DIDResolver::new();
694        let did = "did:q42:z6MkpTHR8VNs";
695        let a = resolver.resolve(did).unwrap();
696        let b = resolver.resolve(did).unwrap();
697        assert_eq!(
698            a.endpoint_url, b.endpoint_url,
699            "resolution must be deterministic"
700        );
701        assert_eq!(a.verification_method, b.verification_method);
702    }
703
704    #[test]
705    fn test_resolver_did_q42_distinct_payloads_distinct_urls() {
706        let resolver = DIDResolver::new();
707        let a = resolver.resolve("did:q42:z6MkpTHR8VNs").unwrap();
708        let b = resolver.resolve("did:q42:z6MkpTHR8VNt").unwrap();
709        assert_ne!(
710            a.endpoint_url, b.endpoint_url,
711            "distinct DIDs must resolve to distinct URLs"
712        );
713    }
714
715    #[test]
716    fn test_resolver_did_web_maps_to_well_known() {
717        let resolver = DIDResolver::new();
718        let did = "did:web:example.com";
719        let result = resolver.resolve(did).expect("did:web must resolve");
720
721        assert_eq!(result.did, did);
722        assert_eq!(
723            result.endpoint_url, "https://example.com/.well-known/did.json",
724            "did:web must map to the standard well-known DID document URL"
725        );
726        assert_eq!(result.verification_method, "did:web:example.com#key-1");
727    }
728
729    #[test]
730    fn test_resolver_did_web_with_path_components() {
731        // did:web uses ':' as a path separator after the domain.
732        let resolver = DIDResolver::new();
733        let did = "did:web:example.com:users:alice";
734        let result = resolver.resolve(did).unwrap();
735        assert_eq!(
736            result.endpoint_url,
737            "https://example.com/users/alice/.well-known/did.json"
738        );
739    }
740
741    #[test]
742    fn test_resolver_did_key() {
743        let resolver = DIDResolver::new();
744        let did = "did:key:z6MkhaXgBZDvotDkL5v7wB9QkN8eYfH2";
745        let result = resolver.resolve(did).expect("did:key must resolve");
746
747        assert_eq!(result.did, did);
748        // did:key has no native HTTP endpoint; point at a universal resolver.
749        assert_eq!(
750            result.endpoint_url,
751            format!("https://dev.uniresolver.io/1.0/identifiers/{}", did)
752        );
753        // The did:key DID is its own verification method.
754        assert_eq!(result.verification_method, did);
755    }
756
757    #[test]
758    fn test_resolver_unknown_method_returns_error() {
759        let resolver = DIDResolver::new();
760        let result = resolver.resolve("did:foo:bar");
761        assert!(result.is_err(), "unknown DID method must error");
762        let err = result.unwrap_err();
763        assert!(
764            err.contains("unsupported DID method"),
765            "error should mention unsupported method, got: {}",
766            err
767        );
768        assert!(err.contains("foo"), "error should name the method 'foo'");
769    }
770
771    #[test]
772    fn test_resolver_non_did_returns_error() {
773        let resolver = DIDResolver::new();
774        let result = resolver.resolve("https://example.com/not-a-did");
775        assert!(result.is_err(), "non-DID input must error");
776        assert!(result.unwrap_err().contains("not a valid DID"));
777    }
778
779    #[test]
780    fn test_resolver_empty_q42_payload_errors() {
781        let resolver = DIDResolver::new();
782        assert!(resolver.resolve("did:q42:").is_err());
783    }
784
785    #[test]
786    fn test_resolver_empty_web_domain_errors() {
787        let resolver = DIDResolver::new();
788        assert!(resolver.resolve("did:web:").is_err());
789    }
790
791    #[test]
792    fn test_resolver_empty_key_errors() {
793        let resolver = DIDResolver::new();
794        assert!(resolver.resolve("did:key:").is_err());
795    }
796
797    #[test]
798    fn test_resolver_resolve_delegates_to_resolve_did() {
799        // `resolve()` and `resolve_did()` must produce identical results.
800        let resolver = DIDResolver::new();
801        let did = "did:web:example.com";
802        let a = resolver.resolve(did).unwrap();
803        let b = resolver.resolve_did(did).unwrap();
804        assert_eq!(a.did, b.did);
805        assert_eq!(a.endpoint_url, b.endpoint_url);
806        assert_eq!(a.verification_method, b.verification_method);
807    }
808
809    #[test]
810    fn test_verify_authentication_signature_fails_closed() {
811        // The SPARQL query layer holds no verifiable public key, so signature
812        // verification must fail closed even when the DID and payload are valid.
813        let resolver = DIDResolver::new();
814        let did = "did:q42:z6MkpTHR8VNs";
815        let result = resolver.verify_authentication_signature(did, &[1u8; 64], &[2u8; 32]);
816        assert!(result.is_err(), "must fail closed, not return valid");
817        assert!(result.unwrap_err().contains("identity/key-vault layer"));
818    }
819
820    #[test]
821    fn test_verify_authentication_signature_rejects_empty_signature() {
822        let resolver = DIDResolver::new();
823        let result =
824            resolver.verify_authentication_signature("did:q42:z6MkpTHR8VNs", &[], &[2u8; 32]);
825        assert!(result.is_err());
826        assert!(result.unwrap_err().contains("empty signature"));
827    }
828
829    #[test]
830    fn test_verify_authentication_signature_rejects_bad_did() {
831        let resolver = DIDResolver::new();
832        // An unresolvable DID should be rejected before any crypto consideration.
833        let result =
834            resolver.verify_authentication_signature("did:foo:bar", &[1u8; 64], &[2u8; 32]);
835        assert!(result.is_err());
836    }
837
838    // ===== ABI-layer pointer resolution (XOR placeholder removed) =====
839
840    #[test]
841    fn test_resolve_did_pointer_is_not_xor_garbage() {
842        // The u64 ABI resolver must no longer use `did ^ 0xDEADBEEF`. Endpoint
843        // and verification-method pointers must be distinct, deterministic, and
844        // in the 60-bit q_hash space (top 4 bits clear).
845        let quins = vec![];
846        let mut handler = SparqlDidHandler::new(&quins);
847        let did = 0x8000000000000001;
848        let result = handler.resolve_did(did).unwrap();
849
850        // Distinct from the old XOR placeholders.
851        assert_ne!(result.endpoint_url, did ^ 0xDEADBEEF);
852        assert_ne!(result.verification_method, did ^ 0xCAFEBABE);
853        // Endpoint and verification method must differ from each other.
854        assert_ne!(result.endpoint_url, result.verification_method);
855        // Must be deterministic.
856        let again = handler.resolve_did(did).unwrap_or_else(|_| {
857            handler.invalidate_cache(did);
858            handler.resolve_did(did).unwrap()
859        });
860        // (cache returns the same value; if cache hit, fields are equal anyway)
861        assert_eq!(result.endpoint_url, again.endpoint_url);
862        // Pointers live in the 60-bit identity space (top 4 bits clear).
863        assert_eq!(result.endpoint_url >> 60, 0);
864        assert_eq!(result.verification_method >> 60, 0);
865    }
866}