Skip to main content

acdp_did/
web.rs

1//! `did:web` resolver — RFC-ACDP-0001 §5.11, step 3.
2
3use acdp_primitives::error::AcdpError;
4
5#[cfg(feature = "client")]
6use {
7    super::document::DidDocument,
8    acdp_primitives::limits::{
9        CONNECT_TIMEOUT, MAX_METADATA_BYTES, MAX_REDIRECTS, REQUEST_TIMEOUT,
10    },
11    acdp_safe_http::SsrfPolicy,
12    lru::LruCache,
13    reqwest::redirect,
14    std::num::NonZeroUsize,
15    std::sync::{Arc, Mutex},
16    std::time::{Duration, Instant},
17};
18
19#[cfg(feature = "client")]
20const CACHE_MAX: Duration = Duration::from_secs(24 * 3600); // 24 hours
21#[cfg(feature = "client")]
22const DEFAULT_CACHE_CAPACITY: usize = 1000;
23
24#[cfg(feature = "client")]
25struct CacheEntry {
26    doc: DidDocument,
27    cached_at: Instant,
28}
29
30/// Resolves `did:web:…` DIDs to DID documents via HTTPS.
31///
32/// Caches resolved documents for 5–24 hours per §5.11 guidance, evicting
33/// the least-recently-used entry once the cache reaches the configured
34/// capacity (default 1000).
35///
36/// Every resolution URL passes the [`SsrfPolicy`] gate before any socket
37/// activity: a producer-controlled `did:web` authority is an SSRF vector
38/// identical to a cross-registry reference (RFC-ACDP-0008 §4.8). The
39/// default policy refuses IP-literal authorities and non-HTTPS schemes,
40/// so `did:web:127.0.0.1` / `did:web:169.254.169.254` cannot turn a
41/// registry verifying a publish — or a consumer verifying a retrieved
42/// context — into an SSRF proxy against process-internal listeners.
43#[cfg(feature = "client")]
44pub struct WebResolver {
45    http: reqwest::Client,
46    cache: Arc<Mutex<LruCache<String, CacheEntry>>>,
47    ssrf_policy: SsrfPolicy,
48    // Stored verbatim so [`Self::with_ssrf_policy`] can rebuild the
49    // HTTP client (the DNS resolver is wired in at builder time, so a
50    // policy swap requires rebuilding).
51    root_cert_pem: Option<Vec<u8>>,
52}
53
54#[cfg(feature = "client")]
55impl WebResolver {
56    /// Build a resolver with the default LRU capacity (1000 entries).
57    ///
58    /// # Panics
59    ///
60    /// Panics if the underlying HTTP client cannot be built (e.g. the
61    /// TLS backend fails to initialize). Use [`Self::try_new`] to
62    /// handle that failure as a `Result` instead.
63    pub fn new() -> Self {
64        Self::try_new().expect("failed to build HTTP client for DID resolver")
65    }
66
67    /// Fallible variant of [`Self::new`]: builds a resolver with the
68    /// default LRU capacity (1000 entries), returning an error instead
69    /// of panicking if the underlying HTTP client cannot be built.
70    pub fn try_new() -> Result<Self, AcdpError> {
71        Self::try_with_capacity(DEFAULT_CACHE_CAPACITY)
72    }
73
74    /// Build a resolver with a custom LRU capacity.
75    ///
76    /// # Panics
77    ///
78    /// Panics if `capacity == 0` — use a positive capacity; the LRU
79    /// model has no semantically valid empty configuration — or if the
80    /// underlying HTTP client cannot be built. Use
81    /// [`Self::try_with_capacity`] to handle the HTTP-client failure
82    /// as a `Result` instead.
83    pub fn with_capacity(capacity: usize) -> Self {
84        Self::try_with_capacity(capacity).expect("failed to build HTTP client for DID resolver")
85    }
86
87    /// Fallible variant of [`Self::with_capacity`]: returns an error
88    /// instead of panicking if the underlying HTTP client cannot be
89    /// built.
90    ///
91    /// # Panics
92    ///
93    /// Still panics if `capacity == 0`; that is a programmer error, not
94    /// a runtime condition.
95    pub fn try_with_capacity(capacity: usize) -> Result<Self, AcdpError> {
96        Self::from_parts(capacity, SsrfPolicy::default(), None)
97    }
98
99    /// Build a resolver that trusts the given PEM-encoded root certificate
100    /// in addition to the system roots.
101    ///
102    /// Primary use is the in-process self-signed HTTPS server in the
103    /// crate's `tests/helpers/tls_did_server.rs` harness, so the spec
104    /// fixtures `pub-001` / `pub-006` / `fed-001..006` can drive the
105    /// resolver end-to-end without going over the network. Production
106    /// callers on corporate intranets MAY also use this to trust a
107    /// private CA.
108    pub fn with_root_cert_pem(pem: &[u8]) -> Result<Self, AcdpError> {
109        Self::from_parts(
110            DEFAULT_CACHE_CAPACITY,
111            SsrfPolicy::default(),
112            Some(pem.to_vec()),
113        )
114    }
115
116    /// Build a resolver with a custom LRU capacity AND a custom root cert.
117    pub fn with_capacity_and_root_cert_pem(capacity: usize, pem: &[u8]) -> Result<Self, AcdpError> {
118        Self::from_parts(capacity, SsrfPolicy::default(), Some(pem.to_vec()))
119    }
120
121    /// Resolver pinned to a fixed socket address for a logical hostname,
122    /// trusting the given root certificate — the resolver-side analogue
123    /// of [`crate::client::RegistryClient::with_test_endpoint`]. Lets a
124    /// test resolve `did:web:localhost` (and path DIDs under it) against
125    /// an in-process TLS server bound to `127.0.0.1:<port>` without the
126    /// port appearing in the DID. Uses the loopback-permitting SSRF
127    /// policy. Use only in tests.
128    #[doc(hidden)]
129    #[cfg(feature = "test-transport")]
130    pub fn with_test_endpoint(
131        pem: &[u8],
132        host: &str,
133        target: std::net::SocketAddr,
134    ) -> Result<Self, AcdpError> {
135        let cap = NonZeroUsize::new(DEFAULT_CACHE_CAPACITY).expect("capacity > 0");
136        let policy = SsrfPolicy::allow_test_loopback();
137        let http = build_http_client_pinned(Some(pem), &policy, Some((host, target)))?;
138        Ok(Self {
139            http,
140            cache: Arc::new(Mutex::new(LruCache::new(cap))),
141            ssrf_policy: policy,
142            root_cert_pem: Some(pem.to_vec()),
143        })
144    }
145
146    fn from_parts(
147        capacity: usize,
148        ssrf_policy: SsrfPolicy,
149        root_cert_pem: Option<Vec<u8>>,
150    ) -> Result<Self, AcdpError> {
151        let cap = NonZeroUsize::new(capacity).expect("WebResolver capacity must be > 0");
152        let http = build_http_client(root_cert_pem.as_deref(), &ssrf_policy)?;
153        Ok(Self {
154            http,
155            cache: Arc::new(Mutex::new(LruCache::new(cap))),
156            ssrf_policy,
157            root_cert_pem,
158        })
159    }
160
161    /// Override the [`SsrfPolicy`] applied to `did:web` resolution.
162    ///
163    /// The policy gates both the URL stage (refusing IP-literal
164    /// authorities and non-HTTPS schemes — fixtures did-ssrf-001/002/003)
165    /// **and** the DNS resolution stage (filtering hostnames that resolve
166    /// into forbidden ranges — RFC-ACDP-0008 §4.8 DNS-rebinding
167    /// protection). Calling this rebuilds the underlying HTTP client so
168    /// the DNS resolver hook reflects the new policy.
169    ///
170    /// Relax the policy **only** in a test harness that resolves
171    /// `did:web:localhost…` against an in-process loopback server.
172    /// Production callers MUST keep the default.
173    ///
174    /// # Panics
175    ///
176    /// Panics if the underlying HTTP client cannot be rebuilt. Use
177    /// [`Self::try_with_ssrf_policy`] to handle that failure as a
178    /// `Result` instead.
179    pub fn with_ssrf_policy(self, policy: SsrfPolicy) -> Self {
180        self.try_with_ssrf_policy(policy)
181            .expect("rebuild HTTP client for DID resolver")
182    }
183
184    /// Fallible variant of [`Self::with_ssrf_policy`]: returns an error
185    /// instead of panicking if the underlying HTTP client cannot be
186    /// rebuilt.
187    pub fn try_with_ssrf_policy(mut self, policy: SsrfPolicy) -> Result<Self, AcdpError> {
188        // Rebuild the HTTP client so the DNS resolver hook carries the
189        // new policy. `build_http_client` is fallible only on bad cert
190        // PEM input; we already validated it at construction time.
191        let http = build_http_client(self.root_cert_pem.as_deref(), &policy)?;
192        self.http = http;
193        self.ssrf_policy = policy;
194        Ok(self)
195    }
196
197    /// Resolve a `did:web:…` DID to a DID document.
198    ///
199    /// Hits the cache on repeated calls for the same DID.  Refreshes
200    /// on any downstream verification failure if needed.
201    #[cfg_attr(feature = "tracing", tracing::instrument(skip(self), fields(did = did)))]
202    pub async fn resolve(&self, did: &str) -> Result<DidDocument, AcdpError> {
203        // Check cache (mutates LRU recency on hit)
204        {
205            let mut cache = self.cache.lock().unwrap();
206            if let Some(entry) = cache.get(did) {
207                if entry.cached_at.elapsed() < CACHE_MAX {
208                    return Ok(entry.doc.clone());
209                }
210            }
211        }
212
213        let url = did_web_to_url(did)?;
214
215        // RFC-ACDP-0008 §4.8: a producer-controlled did:web authority is
216        // an SSRF vector. Refuse loopback / link-local / IMDS / private-
217        // range targets before issuing any request. The refusal is
218        // policy-driven and producer-caused, so it maps to
219        // `key_resolution_failed` (HTTP 400, permanent) — NOT
220        // `key_resolution_unreachable` (HTTP 502, retryable). See
221        // fixtures did-ssrf-001 / did-ssrf-002 / did-ssrf-003.
222        self.ssrf_policy.check_url(&url).map_err(|e| {
223            AcdpError::KeyResolution(format!("SSRF policy blocked did:web resolution: {e}"))
224        })?;
225
226        let mut resp = self
227            .http
228            .get(&url)
229            .header("Accept", "application/did+json, application/json")
230            .send()
231            .await
232            .map_err(|e| classify_reqwest_error(&e))?;
233
234        if !resp.status().is_success() {
235            return Err(AcdpError::KeyResolution(format!(
236                "DID document fetch returned HTTP {}",
237                resp.status()
238            )));
239        }
240
241        // Cap body size at 64 KB per RFC-ACDP-0006 §7.3.
242        if let Some(len) = resp.content_length() {
243            if len as usize > MAX_METADATA_BYTES {
244                return Err(AcdpError::KeyResolution(format!(
245                    "DID document Content-Length {len} exceeds {MAX_METADATA_BYTES}-byte cap"
246                )));
247            }
248        }
249        let mut buf: Vec<u8> = Vec::with_capacity(8 * 1024);
250        while let Some(chunk) = resp
251            .chunk()
252            .await
253            .map_err(|e| AcdpError::KeyResolutionUnreachable(e.to_string()))?
254        {
255            if buf.len() + chunk.len() > MAX_METADATA_BYTES {
256                return Err(AcdpError::KeyResolution(format!(
257                    "DID document body exceeded {MAX_METADATA_BYTES}-byte cap"
258                )));
259            }
260            buf.extend_from_slice(&chunk);
261        }
262        let doc: DidDocument = serde_json::from_slice(&buf)
263            .map_err(|e| AcdpError::KeyResolution(format!("DID document parse: {e}")))?;
264
265        // Store in cache (evicts LRU on overflow)
266        {
267            let mut cache = self.cache.lock().unwrap();
268            cache.put(
269                did.to_string(),
270                CacheEntry {
271                    doc: doc.clone(),
272                    cached_at: Instant::now(),
273                },
274            );
275        }
276
277        Ok(doc)
278    }
279
280    /// Invalidate a specific DID's cache entry, forcing a fresh fetch.
281    pub fn invalidate(&self, did: &str) {
282        self.cache.lock().unwrap().pop(did);
283    }
284}
285
286#[cfg(feature = "client")]
287impl Default for WebResolver {
288    fn default() -> Self {
289        Self::new()
290    }
291}
292
293/// Build the `reqwest::Client` used by `WebResolver`, optionally trusting
294/// an additional PEM-encoded root certificate.
295///
296/// Encapsulates the redirect policy, timeouts, and the DNS-rebinding
297/// filter so the no-cert and with-cert constructors stay byte-for-byte
298/// identical on TLS posture.
299///
300/// `ssrf_policy` is plumbed into reqwest's `dns_resolver` hook via
301/// [`acdp_safe_http::SafeDnsResolver`]: every resolved IP is filtered
302/// against the policy before reqwest connects, so a hostname whose DNS
303/// answers fall in forbidden ranges (loopback, RFC 1918, link-local,
304/// IMDS, ULA, …) is refused at connect time, defeating DNS rebinding
305/// (RFC-ACDP-0008 §4.8).
306#[cfg(feature = "client")]
307fn build_http_client(
308    extra_root_pem: Option<&[u8]>,
309    ssrf_policy: &SsrfPolicy,
310) -> Result<reqwest::Client, AcdpError> {
311    build_http_client_pinned(extra_root_pem, ssrf_policy, None)
312}
313
314/// Like [`build_http_client`] but optionally pins `host` to a fixed
315/// socket address via reqwest's `.resolve()` hook (test endpoints).
316#[cfg(feature = "client")]
317fn build_http_client_pinned(
318    extra_root_pem: Option<&[u8]>,
319    ssrf_policy: &SsrfPolicy,
320    pin: Option<(&str, std::net::SocketAddr)>,
321) -> Result<reqwest::Client, AcdpError> {
322    let policy = redirect::Policy::custom(|attempt| {
323        if attempt.previous().len() >= MAX_REDIRECTS {
324            return attempt.error(format!("DID resolver: exceeded {MAX_REDIRECTS} redirects"));
325        }
326        // Same-authority enforcement (scheme + host + port) against the
327        // original request URL. RFC-ACDP-0008 §4.8.
328        let cross = attempt
329            .previous()
330            .first()
331            .filter(|orig| !acdp_safe_http::same_fetch_authority(orig, attempt.url()))
332            .map(|orig| (orig.to_string(), attempt.url().to_string()));
333        if let Some((from, to)) = cross {
334            return attempt.error(format!(
335                "DID resolver: cross-authority redirect rejected ({from} -> {to})"
336            ));
337        }
338        attempt.follow()
339    });
340
341    let mut builder = reqwest::Client::builder()
342        .use_rustls_tls()
343        .connect_timeout(CONNECT_TIMEOUT)
344        .timeout(REQUEST_TIMEOUT)
345        .redirect(policy)
346        .dns_resolver(acdp_safe_http::SafeDnsResolver::arc(ssrf_policy.clone()));
347
348    if let Some(pem) = extra_root_pem {
349        let cert = reqwest::Certificate::from_pem(pem)
350            .map_err(|e| AcdpError::Http(format!("invalid root cert PEM: {e}")))?;
351        builder = builder.add_root_certificate(cert);
352    }
353
354    if let Some((host, target)) = pin {
355        builder = builder.resolve(host, target);
356    }
357
358    builder
359        .build()
360        .map_err(|e| AcdpError::Http(format!("DID resolver client build: {e}")))
361}
362
363/// Translate a `reqwest::Error` into the right [`AcdpError`] variant.
364///
365/// Walks the error's `source()` chain so the `SafeDnsResolver`'s refusal
366/// (the typed `acdp_primitives::error::SsrfDnsRefusal` marker, detected by
367/// the shared `is_ssrf_refusal`) survives reqwest's wrapping. An
368/// SSRF-refused DNS lookup is policy-driven and
369/// permanent — it maps to `key_resolution_failed` (HTTP 400), NOT
370/// `key_resolution_unreachable` (502, retryable) that
371/// `reqwest::Error::is_connect()` would suggest by default
372/// (RFC-ACDP-0008 §4.8, fixtures did-ssrf-001/002/003).
373#[cfg(feature = "client")]
374fn classify_reqwest_error(e: &reqwest::Error) -> AcdpError {
375    // Shared detection with `From<reqwest::Error> for AcdpError` and
376    // `HttpsDataRefFetcher` (issue #321); only the target variants differ.
377    let chain = acdp_primitives::error::error_chain_message(e);
378    if acdp_primitives::error::is_ssrf_refusal(e) {
379        return AcdpError::KeyResolution(chain);
380    }
381    if e.is_timeout() || e.is_connect() {
382        AcdpError::KeyResolutionUnreachable(chain)
383    } else {
384        AcdpError::KeyResolution(chain)
385    }
386}
387
388/// Convert a `did:web:…` DID to its HTTPS URL per the `did:web` spec.
389///
390/// `did:web:example.com` → `https://example.com/.well-known/did.json`
391/// `did:web:example.com:users:alice` → `https://example.com/users/alice/did.json`
392pub fn did_web_to_url(did: &str) -> Result<String, AcdpError> {
393    let rest = did
394        .strip_prefix("did:web:")
395        .ok_or_else(|| AcdpError::KeyResolution(format!("not a did:web DID: {did}")))?;
396
397    let parts: Vec<&str> = rest.split(':').collect();
398    let authority = urlencoding::decode(parts[0])
399        .map_err(|e| AcdpError::KeyResolution(format!("authority decode: {e}")))?;
400
401    if parts.len() == 1 {
402        Ok(format!("https://{}/.well-known/did.json", authority))
403    } else {
404        let path = parts[1..].join("/");
405        Ok(format!("https://{}/{}/did.json", authority, path))
406    }
407}
408
409/// Convert a registry authority (DNS hostname, optionally with a port)
410/// to its `did:web` form per the did:web method spec.
411///
412/// The `:` between host and port is a structural delimiter in did:web
413/// — it splits the DID into colon-separated path components — so a
414/// `host:port` authority must percent-encode the colon as `%3A` to
415/// keep the port in the authority segment.
416///
417/// Examples:
418/// - `"registry.example.com"`  → `"did:web:registry.example.com"`
419/// - `"localhost:8443"`        → `"did:web:localhost%3A8443"`
420pub fn authority_to_did_web(authority: &str) -> String {
421    let encoded = authority.replace(':', "%3A");
422    format!("did:web:{encoded}")
423}
424
425/// Reverse of [`authority_to_did_web`]: strip the `did:web:` prefix
426/// and decode `%3A` back to `:`. Returns `None` for non-`did:web` input.
427///
428/// Used in `RegistryServer::try_new` and `CrossRegistryResolver::resolve`
429/// to compare a capabilities-advertised DID against the authority the
430/// consumer connected to. Round-trips with `authority_to_did_web`.
431pub fn did_web_to_authority(did: &str) -> Option<String> {
432    let rest = did.strip_prefix("did:web:")?;
433    // Only the first segment carries the authority; further segments
434    // are path components and don't get colon-decoded.
435    let mut parts = rest.splitn(2, ':');
436    let authority = parts.next()?;
437    Some(authority.replace("%3A", ":"))
438}
439
440#[cfg(test)]
441mod tests {
442    use super::*;
443
444    #[test]
445    fn bare_authority() {
446        let url = did_web_to_url("did:web:example.com").unwrap();
447        assert_eq!(url, "https://example.com/.well-known/did.json");
448    }
449
450    #[test]
451    fn path_authority() {
452        let url = did_web_to_url("did:web:example.com:users:alice").unwrap();
453        assert_eq!(url, "https://example.com/users/alice/did.json");
454    }
455
456    /// BUG-06: bare DNS hostname maps to the plain did:web form.
457    #[test]
458    fn authority_to_did_web_bare_hostname() {
459        assert_eq!(
460            authority_to_did_web("registry.example.com"),
461            "did:web:registry.example.com"
462        );
463    }
464
465    /// BUG-06: `host:port` authority percent-encodes the colon.
466    #[test]
467    fn authority_to_did_web_with_port() {
468        assert_eq!(
469            authority_to_did_web("localhost:8443"),
470            "did:web:localhost%3A8443"
471        );
472    }
473
474    /// BUG-06: reverse helper strips prefix and decodes the colon.
475    #[test]
476    fn did_web_to_authority_round_trips() {
477        for authority in ["registry.example.com", "localhost:8443", "127.0.0.1:9000"] {
478            let did = authority_to_did_web(authority);
479            let back = did_web_to_authority(&did)
480                .unwrap_or_else(|| panic!("did_web_to_authority returned None for '{did}'"));
481            assert_eq!(back, authority, "round-trip for '{authority}' failed");
482        }
483    }
484
485    /// `did_web_to_url` already URL-decodes the authority — confirm
486    /// that `authority_to_did_web("localhost:8443")` produces a DID
487    /// that resolves to `https://localhost:8443/.well-known/did.json`.
488    #[test]
489    fn authority_to_did_web_then_to_url_keeps_port() {
490        let did = authority_to_did_web("localhost:8443");
491        let url = did_web_to_url(&did).unwrap();
492        assert_eq!(url, "https://localhost:8443/.well-known/did.json");
493    }
494
495    // ── BUG-04 — WebResolver SSRF policy (did-ssrf-001/002/003) ─────────
496
497    /// did-ssrf-001 — a `did:web` authority that is a loopback IP literal
498    /// is refused by the default resolver before any socket activity.
499    /// The error is `key_resolution_failed` (permanent), not
500    /// `key_resolution_unreachable` (retryable).
501    #[cfg(feature = "client")]
502    #[tokio::test]
503    async fn did_resolver_rejects_loopback_did() {
504        let resolver = WebResolver::new();
505        let err = resolver.resolve("did:web:127.0.0.1").await.unwrap_err();
506        assert!(
507            matches!(err, AcdpError::KeyResolution(_)),
508            "did-ssrf-001: loopback did:web MUST be blocked by SSRF policy, got {err:?}"
509        );
510    }
511
512    /// did-ssrf-002 — a `did:web` authority pointing at the cloud-metadata
513    /// endpoint (169.254.169.254) is refused.
514    #[cfg(feature = "client")]
515    #[tokio::test]
516    async fn did_resolver_rejects_imds_did() {
517        let resolver = WebResolver::new();
518        let err = resolver
519            .resolve("did:web:169.254.169.254")
520            .await
521            .unwrap_err();
522        assert!(
523            matches!(err, AcdpError::KeyResolution(_)),
524            "did-ssrf-002: IMDS did:web MUST be blocked by SSRF policy, got {err:?}"
525        );
526    }
527
528    /// did-ssrf-003 — a `did:web` authority in an RFC 1918 private range
529    /// is refused.
530    #[cfg(feature = "client")]
531    #[tokio::test]
532    async fn did_resolver_rejects_private_range_did() {
533        let resolver = WebResolver::new();
534        for did in [
535            "did:web:192.168.1.1",
536            "did:web:10.0.0.1",
537            "did:web:172.16.0.1",
538        ] {
539            let err = resolver.resolve(did).await.unwrap_err();
540            assert!(
541                matches!(err, AcdpError::KeyResolution(_)),
542                "did-ssrf-003: private-range did:web '{did}' MUST be blocked, got {err:?}"
543            );
544        }
545    }
546
547    /// RFC-ACDP-0008 §4.8 DNS-rebinding protection — a hostname whose
548    /// DNS answers fall in forbidden ranges is refused at the DNS step,
549    /// before any TCP connect. `localhost` is a perfectly valid DNS
550    /// name (it passes `check_url`), but it resolves to `127.0.0.1` —
551    /// which the default policy MUST refuse via the `SafeDnsResolver`
552    /// hook on reqwest's `dns_resolver`. The error message MUST
553    /// identify the SSRF policy so operators can tell the refusal
554    /// apart from a generic connection failure.
555    #[cfg(feature = "client")]
556    #[tokio::test]
557    async fn did_resolver_rejects_hostname_resolving_to_loopback() {
558        let resolver = WebResolver::new();
559        let err = resolver
560            .resolve("did:web:localhost%3A12345")
561            .await
562            .expect_err("DNS-rebinding protection MUST refuse localhost under default policy");
563        let msg = format!("{err}");
564        // SSRF-refused DNS is policy-driven and permanent — it maps to
565        // `KeyResolution` (HTTP 400), NOT `KeyResolutionUnreachable`
566        // (HTTP 502, retryable). The retry-aware client MUST NOT retry.
567        assert!(
568            matches!(err, AcdpError::KeyResolution(_)),
569            "DNS-rebinding refusal MUST be permanent KeyResolution, got {err:?}"
570        );
571        assert!(
572            msg.contains("SSRF policy"),
573            "DNS-rebinding refusal MUST identify the SSRF policy in its message; got: {msg}"
574        );
575        // And `is_transient` MUST return false so retry loops don't loop.
576        assert!(!err.is_transient(), "SSRF refusal MUST NOT be transient");
577    }
578}