identity_kit/did/method.rs
1//! `DidMethod`: a DID method name with an optional namespace, such as `did:demo` or
2//! `did:demo:dev`.
3
4use core::fmt;
5use core::str::FromStr;
6
7use serde::de::Error as _;
8use serde::{Deserialize, Deserializer, Serialize, Serializer};
9
10use super::DidError;
11use super::syntax::{
12 DEFAULT_MAX_LEN, Did, check_len, is_idchar, is_method_char, is_pct_encoded_at,
13};
14
15/// A DID method, named with an optional namespace: `did:demo` or `did:demo:dev`.
16///
17/// Parties that declare or negotiate support for DID methods (SIOPv2's
18/// `subject_syntax_types_supported`, a wallet's configuration, a resolver's registry) exchange
19/// method identifiers rather than DIDs. A `DidMethod` is such an identifier: the method name of
20/// [DID Core §3.1] (`method-name = 1*method-char`, lowercase ASCII letters and digits only),
21/// optionally followed by one `idchar` segment that names a namespace within the method, as
22/// `dev` in `did:demo:dev` or `zXYZ` in `did:ebsi:zXYZ`. SIOPv2 lists methods without a
23/// namespace; wallets and issuers that distinguish networks of one method exchange the namespaced
24/// form; both parse.
25///
26/// A `DidMethod` is validated when it is constructed and keeps the input unchanged. It displays,
27/// serializes and deserializes as `did:<name>` or `did:<name>:<namespace>`, where
28/// deserialization validates. Equality is field-wise; the ordering is by name, then namespace,
29/// with the bare method before its namespaced forms.
30///
31/// [`DidMethod::matches`] tells whether a [`Did`] belongs to the method: its method name is
32/// equal and, when a namespace is set, the first colon-separated segment of its method-specific
33/// id is that namespace. [`Did::method`] keeps returning the name as a `&str`;
34/// `DidMethod::from(&did)` is the method of a DID, without a namespace.
35///
36/// # Errors
37///
38/// A method name that is empty or has a character outside `a-z` and `0-9` is
39/// [`DidError::InvalidMethodName`], as it is for a [`Did`]. A namespace that is empty, has a
40/// second segment (`did:demo:dev:x`) or a character that is not an `idchar` is
41/// [`DidError::InvalidNamespace`]; an incomplete percent-encoding in it is
42/// [`DidError::InvalidPercentEncoding`]. Parsing a string also reports
43/// [`DidError::MissingScheme`] and [`DidError::TooLong`] as [`Did::parse`] does. Positions are
44/// byte offsets into the input that was given: the string for the parsing forms, the name for
45/// [`DidMethod::new`], the namespace for [`DidMethod::with_namespace`].
46///
47/// # Examples
48///
49/// ```
50/// use identity_kit::did::{Did, DidError, DidMethod};
51///
52/// let demo: DidMethod = "did:demo".parse()?;
53/// let namespaced = DidMethod::new("demo")?.with_namespace("dev")?;
54/// assert_eq!(demo.name(), "demo");
55/// assert_eq!(demo.namespace(), None);
56/// assert_eq!(namespaced.namespace(), Some("dev"));
57/// assert_eq!(namespaced.to_string(), "did:demo:dev");
58///
59/// let did = Did::parse("did:demo:dev:0xabc")?;
60/// assert!(demo.matches(&did));
61/// assert!(namespaced.matches(&did));
62/// assert!(!DidMethod::parse("did:demo:alt")?.matches(&did));
63/// assert_eq!(DidMethod::from(&did), demo);
64///
65/// // The method name is lowercase; the namespace is one non-empty segment.
66/// assert_eq!(
67/// DidMethod::parse("did:Example"),
68/// Err(DidError::InvalidMethodName { position: 4 })
69/// );
70/// assert_eq!(
71/// DidMethod::parse("did:demo:"),
72/// Err(DidError::InvalidNamespace { position: 9 })
73/// );
74/// assert_eq!(
75/// DidMethod::parse("did:demo:dev:x"),
76/// Err(DidError::InvalidNamespace { position: 12 })
77/// );
78/// # Ok::<(), DidError>(())
79/// ```
80///
81/// [DID Core §3.1]: https://www.w3.org/TR/did-core/#did-syntax
82#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
83pub struct DidMethod {
84 /// The method name: `demo` in `did:demo:dev`.
85 name: String,
86 /// The namespace within the method: `dev` in `did:demo:dev`.
87 namespace: Option<String>,
88}
89
90impl DidMethod {
91 /// A method named `name`, without a namespace.
92 ///
93 /// # Errors
94 ///
95 /// Returns [`DidError::InvalidMethodName`] when `name` is empty or has a character outside
96 /// `a-z` and `0-9`, with the byte offset into `name`.
97 ///
98 /// # Examples
99 ///
100 /// ```
101 /// use identity_kit::did::{DidError, DidMethod};
102 ///
103 /// assert_eq!(DidMethod::new("web")?.to_string(), "did:web");
104 /// assert_eq!(
105 /// DidMethod::new("ex_ample"),
106 /// Err(DidError::InvalidMethodName { position: 2 })
107 /// );
108 /// # Ok::<(), DidError>(())
109 /// ```
110 pub fn new(name: impl Into<String>) -> Result<Self, DidError> {
111 let name = name.into();
112 validate_name(&name, 0)?;
113 Ok(Self {
114 name,
115 namespace: None,
116 })
117 }
118
119 /// This method with its namespace set to `namespace`.
120 ///
121 /// # Errors
122 ///
123 /// Returns [`DidError::InvalidNamespace`] when `namespace` is empty or has a character that
124 /// is not an `idchar` (a `:` among them), or [`DidError::InvalidPercentEncoding`] when a `%`
125 /// is not followed by two hexadecimal digits, with the byte offset into `namespace`.
126 ///
127 /// # Examples
128 ///
129 /// ```
130 /// use identity_kit::did::{DidError, DidMethod};
131 ///
132 /// let ebsi = DidMethod::new("ebsi")?;
133 /// assert_eq!(ebsi.clone().with_namespace("zXYZ")?.to_string(), "did:ebsi:zXYZ");
134 /// assert_eq!(
135 /// ebsi.with_namespace("a:b"),
136 /// Err(DidError::InvalidNamespace { position: 1 })
137 /// );
138 /// # Ok::<(), DidError>(())
139 /// ```
140 pub fn with_namespace(self, namespace: impl Into<String>) -> Result<Self, DidError> {
141 let namespace = namespace.into();
142 validate_namespace(&namespace, 0)?;
143 Ok(Self {
144 name: self.name,
145 namespace: Some(namespace),
146 })
147 }
148
149 /// Parses `did:<name>` or `did:<name>:<namespace>`, rejecting input longer than
150 /// [`DEFAULT_MAX_LEN`] bytes.
151 ///
152 /// # Errors
153 ///
154 /// Returns a [`DidError`] describing the first violation, as listed for [`DidMethod`].
155 ///
156 /// # Examples
157 ///
158 /// ```
159 /// use identity_kit::did::{DidError, DidMethod};
160 ///
161 /// let method = DidMethod::parse("did:ebsi:zXYZ")?;
162 /// assert_eq!(method.name(), "ebsi");
163 /// assert_eq!(method.namespace(), Some("zXYZ"));
164 ///
165 /// assert_eq!(DidMethod::parse("ebsi"), Err(DidError::MissingScheme));
166 /// # Ok::<(), DidError>(())
167 /// ```
168 pub fn parse(s: &str) -> Result<Self, DidError> {
169 check_len(s, DEFAULT_MAX_LEN)?;
170 let Some(rest) = s.strip_prefix(SCHEME) else {
171 return Err(DidError::MissingScheme);
172 };
173 let (name, namespace) = match rest.split_once(':') {
174 Some((name, namespace)) => (name, Some(namespace)),
175 None => (rest, None),
176 };
177 validate_name(name, SCHEME.len())?;
178 if let Some(namespace) = namespace {
179 validate_namespace(namespace, SCHEME.len() + name.len() + 1)?;
180 }
181 Ok(Self {
182 name: name.to_owned(),
183 namespace: namespace.map(str::to_owned),
184 })
185 }
186
187 /// The method name: `demo` in `did:demo:dev`.
188 pub fn name(&self) -> &str {
189 &self.name
190 }
191
192 /// The namespace within the method: `dev` in `did:demo:dev`, `None` in `did:demo`.
193 pub fn namespace(&self) -> Option<&str> {
194 self.namespace.as_deref()
195 }
196
197 /// Whether `did` belongs to this method: its method name is [`name`](Self::name) and, when a
198 /// namespace is set, the first colon-separated segment of its method-specific id is that
199 /// namespace.
200 ///
201 /// # Examples
202 ///
203 /// ```
204 /// use identity_kit::did::{Did, DidMethod};
205 ///
206 /// let did = Did::parse("did:demo:dev:0xabc")?;
207 /// assert!(DidMethod::parse("did:demo")?.matches(&did));
208 /// assert!(DidMethod::parse("did:demo:dev")?.matches(&did));
209 /// assert!(!DidMethod::parse("did:demo:alt")?.matches(&did));
210 /// assert!(!DidMethod::parse("did:web")?.matches(&did));
211 /// # Ok::<(), identity_kit::did::DidError>(())
212 /// ```
213 pub fn matches(&self, did: &Did) -> bool {
214 if did.method() != self.name {
215 return false;
216 }
217 match &self.namespace {
218 None => true,
219 Some(namespace) => {
220 let id = did.method_specific_id();
221 let first = id.split_once(':').map_or(id, |(first, _)| first);
222 first == namespace
223 }
224 }
225 }
226}
227
228/// The method of a DID, without a namespace: `did:demo` for `did:demo:dev:0xabc`.
229impl From<&Did> for DidMethod {
230 fn from(did: &Did) -> Self {
231 Self {
232 name: did.method().to_owned(),
233 namespace: None,
234 }
235 }
236}
237
238impl fmt::Display for DidMethod {
239 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
240 f.write_str(SCHEME)?;
241 f.write_str(&self.name)?;
242 if let Some(namespace) = &self.namespace {
243 f.write_str(":")?;
244 f.write_str(namespace)?;
245 }
246 Ok(())
247 }
248}
249
250impl FromStr for DidMethod {
251 type Err = DidError;
252
253 fn from_str(s: &str) -> Result<Self, Self::Err> {
254 Self::parse(s)
255 }
256}
257
258impl TryFrom<&str> for DidMethod {
259 type Error = DidError;
260
261 fn try_from(s: &str) -> Result<Self, Self::Error> {
262 Self::parse(s)
263 }
264}
265
266impl TryFrom<String> for DidMethod {
267 type Error = DidError;
268
269 fn try_from(s: String) -> Result<Self, Self::Error> {
270 Self::parse(&s)
271 }
272}
273
274impl Serialize for DidMethod {
275 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
276 serializer.collect_str(self)
277 }
278}
279
280impl<'de> Deserialize<'de> for DidMethod {
281 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
282 let s = String::deserialize(deserializer)?;
283 Self::parse(&s).map_err(D::Error::custom)
284 }
285}
286
287const SCHEME: &str = "did:";
288
289/// Checks `name` against `method-name = 1*method-char`; positions are reported from `offset`.
290fn validate_name(name: &str, offset: usize) -> Result<(), DidError> {
291 if name.is_empty() {
292 return Err(DidError::InvalidMethodName { position: offset });
293 }
294 match name.bytes().position(|c| !is_method_char(c)) {
295 None => Ok(()),
296 Some(i) => Err(DidError::InvalidMethodName {
297 position: offset + i,
298 }),
299 }
300}
301
302/// Checks `namespace` against `1*idchar`; positions are reported from `offset`.
303///
304/// The scanner only advances over ASCII bytes, so every position it reports is a `char` boundary.
305fn validate_namespace(namespace: &str, offset: usize) -> Result<(), DidError> {
306 let bytes = namespace.as_bytes();
307 if bytes.is_empty() {
308 return Err(DidError::InvalidNamespace { position: offset });
309 }
310 let mut i = 0;
311 while i < bytes.len() {
312 match bytes[i] {
313 b'%' if is_pct_encoded_at(bytes, i) => i += 3,
314 b'%' => {
315 return Err(DidError::InvalidPercentEncoding {
316 position: offset + i,
317 });
318 }
319 c if is_idchar(c) => i += 1,
320 _ => {
321 return Err(DidError::InvalidNamespace {
322 position: offset + i,
323 });
324 }
325 }
326 }
327 Ok(())
328}