pk11_uri_parser/lib.rs
1//! A *zero-copy* library to parse and validate PKCS#11 URIs in accordance to [RFC7512][rfc7512] specifications.
2//!
3//!
4//!
5//! [rfc7512]: <https://datatracker.ietf.org/doc/html/rfc7512>
6//!
7//! ## Examples
8//!
9//! Using a sample URI from the specification:
10//! ```
11//! use pk11_uri_parser::{parse, PK11URIError};
12//!
13//! fn main() -> Result<(), PK11URIError> {
14//! let pk11_uri = "pkcs11:token=The%20Software%20PKCS%2311%20Softtoken;
15//! manufacturer=Snake%20Oil,%20Inc.;
16//! model=1.0;
17//! object=my-certificate;
18//! type=cert;
19//! id=%69%95%3E%5C%F4%BD%EC%91;
20//! serial=
21//! ?pin-source=file:/etc/token_pin";
22//!
23//! let mapping = parse(pk11_uri)?;
24//!
25//! println!("{mapping:?}");
26//! Ok(())
27//! }
28//! ```
29//! Will effectively print:
30//! ```terminal
31//! PK11URIMapping { token: Some("The%20Software%20PKCS%2311%20Softtoken"), manufacturer: Some("Snake%20Oil,%20Inc."), serial: Some(""), model: Some("1.0"), library_manufacturer: None, library_version: None, library_description: None, object: Some("my-certificate"), type: Some("cert"), id: Some("%69%95%3E%5C%F4%BD%EC%91"), slot_description: None, slot_manufacturer: None, slot_id: None, pin_source: Some("file:/etc/token_pin"), pin_value: None, module_name: None, module_path: None, vendor: {} }
32//! ```
33//!
34//! The [parse] `Result`'s type is a [PK11URIMapping]. Users of the library do not need to be intimately
35//! familiar with specification rules regarding what attributes belong to the path-component or the
36//! query-component, or to be knowledgeable about the various vendor-specific attribute rules: the `PK11URIMapping`
37//! provides appropriately named methods for retrieving standard component values and an intuitive
38//! [vendor][`PK11URIMapping::vendor()`] method for retrieving *vendor-specific* attribute values.
39//! ```
40//! let pk11_uri = "pkcs11:vendor-attribute=my_vendor_attribte?pin-source=|/usr/lib/pinomatic";
41//! let mapping = pk11_uri_parser::parse(pk11_uri).expect("mapping should be valid");
42//! if let Some(pin_source) = mapping.pin_source() {
43//! // do something with `pin_source`...
44//! }
45//! // see whether we've got `vendor-attribute` values:
46//! if let Some(vendor_values) = mapping.vendor("vendor-attribute") {
47//! // do something with `vendor_values`...
48//! }
49//! ```
50//!
51//! It's worth reiterating that vendor-specific attributes may have *multiple* values so therefore the `vendor`
52//! method's `Option` return type is `&Vec<&'a str>`.
53//!
54//! ## Errors
55//!
56//! At least initially, PKCS#11 URIs will likely be derived from invoking exploratory commands in tools such as
57//! `p11tool` or `pkcs11-tool`. While parsing URIs from these tools is pretty much guaranteed to be successful,
58//! it's often *not* necessary to provide such verbose values in order to properly identify your targeted resource.
59//! It's also generally beyond the scope of those tools to include query-components (such as `pin-value` or `pin-source`).
60//! In the interest of making your life a little bit easier (and code more readable), a bit of exploration can result
61//! in a considerably shorter (and potentially more *portable*) URI.
62//!
63//! Let's say for example you are in need of utilizing an HSM-bound private key (and read "somewhere on the internet"):
64//! ```
65//! // note: this isn't a valid pkcs11 uri
66//! let pk11_uri = "pkcs11:object=Private key for Card Authentication;pin-value=123456";
67//! #[cfg(feature = "validation")]
68//! println!("{err:?}", err=pk11_uri_parser::parse(pk11_uri).expect_err("empty spaces in value violation"));
69//! ```
70//! Attempting to parse that uri will result in a [PK11URIError].
71//! ```terminal
72//! PK11URIError { pk11_uri: "pkcs11:object=Private key for Card Authentication;pin-value=123456", error_span: (7, 49), violation: "Invalid component value: Appendix A of [RFC3986] specifies component values may not contain empty spaces.", help: "Replace `Private key for Card Authentication` with `Private%20key%20for%20Card%20Authentication`." }
73//! ```
74//! Or if you'd prefer a fancier output, simply display the PK11URIError (*not* using `:?` debug):
75//! ```
76//! // note: this isn't a valid pkcs11 uri
77//! let pk11_uri = "pkcs11:object=Private key for Card Authentication;pin-value=123456";
78//! #[cfg(feature = "validation")]
79//! println!("{err}", err=pk11_uri_parser::parse(pk11_uri).expect_err("empty spaces in value violation"))
80//! ```
81//! ```terminal
82//! pkcs11:object=Private key for Card Authentication;pin-value=123456
83//! ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Invalid component value: Appendix A of [RFC3986] specifies component values may not contain empty spaces.
84//!
85//! help: Replace `Private key for Card Authentication` with `Private%20key%20for%20Card%20Authentication`.
86//! ```
87//! Great! Based on the "help" text, it's a simple fix:
88//! ```
89//! // note: again, this isn't a valid pkcs11 uri
90//! let pk11_uri = "pkcs11:object=Private%20key%20for%20Card%20Authentication;pin-value=123456";
91//! #[cfg(feature = "validation")]
92//! println!("{err}", err=pk11_uri_parser::parse(pk11_uri).expect_err("query component naming collision violation"));
93//! ```
94//! This will once again fail to parse and brings up the fact that this library will *fail-quickly* (ie, short-circuit *further* parsing) if any violation is found.
95//! ```terminal
96//! pkcs11:object=Private%20key%20for%20Card%20Authentication;pin-value=123456
97//! ^^^^^^^^^^^^^^^^ Naming collision with standard query component.
98//!
99//! help: Move `pin-value` and its value to the PKCS#11 URI query.
100//! ```
101//! In this case, `pin-value` is a standard *query-component* attribute name so its current location as a path attribute is a violation.
102//! The "help" section again offers a simple solution.
103//! ```no_run
104//! let pk11_uri = "pkcs11:object=Private%20key%20for%20Card%20Authenciation?pin-value=123456";
105//! pk11_uri_parser::parse(pk11_uri).expect("mapping should be valid");
106//! ```
107//! Which finally yields a valid mapping.
108//!
109//! ## Warnings
110//!
111//! The [RFC7512][rfc7512] specification uses terminology such as `SHOULD` and `SHOULD NOT` to indicate *optional*,
112//! best-practice type treatment for attribute values. This library embraces these optional rules, but will only
113//! emit *warning* messages to the terminal and only provide such warnings for *non-optimized* builds. Likewise,
114//! violations of such optional rules will *never* result in a [PK11URIError]. The messages printed to the terminal
115//! begin with `pkcs11 warning:`.
116//!
117//! Assuming a debug build:
118//! ```no_run
119//! let pk11_uri = "pkcs11:x-muppet=cookie<^^>monster!";
120//! let mapping = pk11_uri_parser::parse(pk11_uri).expect("mapping should be valid");
121//! let x_muppet = mapping.vendor("x-muppet").expect("valid x-muppet vendor-attribute");
122//! println!("x-muppet: {:?}", x_muppet);
123//! ```
124//! prints
125//! ```terminal
126//! pkcs11 warning: per RFC7512, the previously used convention of starting vendor attributes with an "x-" prefix is now deprecated. Identified: `x-muppet`.
127//! pkcs11 warning: the `<` identified at offset 6 in `cookie<^^>monster!` of component `x-muppet=cookie<^^>monster!` SHOULD be percent-encoded.
128//! pkcs11 warning: the `^` identified at offset 7 in `cookie<^^>monster!` of component `x-muppet=cookie<^^>monster!` SHOULD be percent-encoded.
129//! pkcs11 warning: the `^` identified at offset 8 in `cookie<^^>monster!` of component `x-muppet=cookie<^^>monster!` SHOULD be percent-encoded.
130//! pkcs11 warning: the `>` identified at offset 9 in `cookie<^^>monster!` of component `x-muppet=cookie<^^>monster!` SHOULD be percent-encoded.
131//! x-muppet: ["cookie<^^>monster!"]
132//! ```
133//! Any warning related code is explicitly **not** included in `--release` builds.
134//!
135//! ## Crate feature flags
136//!
137//! As alluded to above, the crate's **default** feature set is to *always* perform validation and for
138//! debug builds, emit `pkcs11 warning:` messages when values do not comply with RFC7512 "SHOULD/
139//! SHOULD NOT" guidelines.
140//!
141//! > "But sir, I implore you, I've *thoroughly* tested my input!"
142//!
143//! I hear you barking, big dog! It's perfectly reasonable to *not* want validation (and/or warnings). You
144//! can eliminate that slight bit of runtime overhead by utilizing the `default-features=false` treatment
145//! on your dependency:
146//! ```toml
147//! [dependencies]
148//! pk11-uri-parser = {version = "0.1.4", default-features = false}
149//! ```
150//! It's important to note, however, that doing so will introduce `expect("my expectation")` calls to perform
151//! unwrap functionality required in the parsing.
152
153use core::error;
154use std::collections::HashMap;
155use std::fmt;
156
157#[macro_use]
158mod macros;
159
160mod common;
161mod pk11_pattr;
162mod pk11_qattr;
163
164const PKCS11_SCHEME: &str = "pkcs11:";
165const PKCS11_SCHEME_LEN: usize = PKCS11_SCHEME.len();
166
167/// Issued when [parsing][parse] a PKCS#11 URI is found to be in violation of [RFC7512][rfc7512] specifications.
168///
169/// The included `pk11_uri` is a "tidied" version of the one provided to the
170/// `parse` function: any *newline* or *tab* formatting has been stripped out
171/// in order to accurately identify the `error_span` within the uri. The `violation`
172/// will refer to the [RFC7512 Augmented BNF][abnf] whenever possible, while the `help`
173/// value provides a more human-friendly suggestion to correcting the violation.
174///
175/// [rfc7512]: <https://datatracker.ietf.org/doc/html/rfc7512>
176/// [abnf]: <https://datatracker.ietf.org/doc/html/rfc7512#section-2.3>
177#[derive(Debug)]
178pub struct PK11URIError {
179 /// The tidied uri identified as violating RFC7512.
180 pk11_uri: String,
181 /// The start end end offsets of the error.
182 error_span: (usize, usize),
183 /// The ABNF or RFC7512 text exhibiting the issue.
184 violation: String,
185 /// Human-friendly suggestion of how to resolve the issue.
186 help: String,
187}
188
189impl error::Error for PK11URIError {}
190
191/// Highlights the issue using the `error_span`.
192impl fmt::Display for PK11URIError {
193 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
194 let padding = self.error_span.0;
195 let highlight = self.error_span.1 - padding;
196 write!(
197 f,
198 "{}\n{:padding$}{:^^highlight$} {violation}\n\nhelp: {help}",
199 self.pk11_uri,
200 "",
201 "^",
202 violation = self.violation,
203 help = self.help
204 )
205 }
206}
207
208/// Encapsulates the result of successfully [parsing][parse] a PKCS#11 URI.
209#[derive(Debug, Default, Clone)]
210pub struct PK11URIMapping<'a> {
211 // pk11-pattr:
212 token: Option<&'a str>,
213 manufacturer: Option<&'a str>,
214 serial: Option<&'a str>,
215 model: Option<&'a str>,
216 library_manufacturer: Option<&'a str>,
217 library_version: Option<&'a str>,
218 library_description: Option<&'a str>,
219 object: Option<&'a str>,
220 r#type: Option<&'a str>,
221 id: Option<&'a str>,
222 slot_description: Option<&'a str>,
223 slot_manufacturer: Option<&'a str>,
224 slot_id: Option<&'a str>,
225 // pk11-qattr:
226 pin_source: Option<&'a str>,
227 pin_value: Option<&'a str>,
228 module_name: Option<&'a str>,
229 module_path: Option<&'a str>,
230 // vendor-specific:
231 vendor: HashMap<&'a str, Vec<&'a str>>,
232}
233
234impl<'a> PK11URIMapping<'a> {
235 // pk11-pattr:
236 attr_access!(token for pk11-pattr "token");
237 attr_access!(manufacturer for pk11-pattr "manufacturer");
238 attr_access!(serial for pk11-pattr "serial");
239 attr_access!(model for pk11-pattr "model");
240 attr_access!(library_manufacturer for pk11-pattr "library-manufacturer");
241 attr_access!(library_version for pk11-pattr "library-version");
242 attr_access!(library_description for pk11-pattr "library-description");
243 attr_access!(object for pk11-pattr "object");
244 attr_access!(r#type for pk11-pattr "type");
245 attr_access!(id for pk11-pattr "id");
246 attr_access!(slot_description for pk11-pattr "slot-description");
247 attr_access!(slot_manufacturer for pk11-pattr "slot-manufacturer");
248 attr_access!(slot_id for pk11-pattr "slot-id");
249 // pk11-qattr:
250 attr_access!(pin_source for pk11-qattr "pin-source");
251 attr_access!(pin_value for pk11-qattr "pin-value");
252 attr_access!(module_name for pk11-qattr "module-name");
253 attr_access!(module_path for pk11-qattr "module-path");
254 // vendor-specific:
255 /// Retrieve the `&Vec<&'a str>` values for the *vendor-specific* `vendor_attr` if parsed.
256 ///
257 /// ## Examples
258 ///
259 ///```
260 /// // `v-attr` is an example "vendor-specific" attribute:
261 /// let pk11_uri = "pkcs11:v-attr=val1?v-attr=val2&v-attr=val3";
262 /// let mapping = pk11_uri_parser::parse(pk11_uri).expect("valid mapping");
263 /// // Retrieve the `v-attr` values using the `vendor` method:
264 /// let vendor_attrs = mapping.vendor("v-attr").expect("v-attr vendor-specific attribute values");
265 /// for v_attr_val in vendor_attrs {
266 /// println!("{v_attr_val}")
267 /// }
268 /// ```
269 /// prints
270 /// ```terminal
271 /// val1
272 /// val2
273 /// val3
274 /// ```
275 pub fn vendor(&self, vendor_attr: &str) -> Option<&Vec<&'a str>> {
276 self.vendor.get(vendor_attr)
277 }
278}
279
280/// Parses and verifies the contents of the given `pk11_uri` &str, making
281/// parsed values available through a [PK11URIMapping]. Violations to [RFC7512][rfc7512]
282/// specifications will result in issuing a [PK11URIError].
283///
284/// The contents of the `PK11URIMapping` are string slices of the `pk11_uri`,
285/// so if you need the mapping to outlive the pk11_uri, simply clone it.
286///
287/// [rfc7512]: <https://datatracker.ietf.org/doc/html/rfc7512>
288pub fn parse(pk11_uri: &str) -> Result<PK11URIMapping, PK11URIError> {
289 #[cfg(feature = "validation")]
290 if !pk11_uri.starts_with(PKCS11_SCHEME) {
291 return Err(PK11URIError {
292 pk11_uri: tidy(pk11_uri),
293 error_span: (0, 0),
294 violation: String::from(
295 r#"Invalid `pk11-URI`: expected `"pkcs11:" pk11-path [ "?" pk11-query ]`."#,
296 ),
297 help: String::from("PKCS#11 URI must start with `pkcs11:`."),
298 });
299 }
300
301 // Technically, a lone `pkcs11:` scheme is valid, so
302 // we'll go ahead and create our default mapping now:
303 let mut mapping = PK11URIMapping::default();
304
305 let query_component_index = pk11_uri.find('?');
306
307 // If we've got a `pk11-path`, attempt to assign its `pk11-pattr` values:
308 if let Some(pk11_path) = pk11_uri
309 .get(PKCS11_SCHEME_LEN..query_component_index.unwrap_or(pk11_uri.len()))
310 .filter(|pk11_path| !pk11_path.is_empty())
311 {
312 pk11_path
313 .split(';')
314 .enumerate()
315 .try_for_each(|(count, pk11_pattr)| {
316 pk11_pattr::assign(pk11_pattr, &mut mapping).map_err(|validation_err| {
317 let tidy_pk11_uri = tidy(pk11_uri);
318 let tidy_pk11_path = tidy(pk11_path);
319 let tidy_pk11_pattr = tidy(pk11_pattr);
320
321 let mut violation = validation_err.violation;
322 let mut help = validation_err.help;
323
324 let error_start = if !tidy_pk11_pattr.is_empty() {
325 tidy_pk11_path.find(&tidy_pk11_pattr).unwrap()
326 } else {
327 // assign this here rather than adding O(n) runtime checks
328 // for basically an unlikely outlier type of error:
329 violation = String::from("Misplaced path delimiter.");
330 help = String::from("Remove the misplaced ';' delimiter.");
331 find_empty_attr_index(&tidy_pk11_path, count, ';')
332 } + PKCS11_SCHEME_LEN;
333 PK11URIError {
334 pk11_uri: tidy_pk11_uri,
335 error_span: (error_start, error_start + tidy_pk11_pattr.len()),
336 violation,
337 help,
338 }
339 })
340 })?;
341 }
342
343 // If we've got a `pk11-query`, attempt to assign its `pk11-qattr` values:
344 if query_component_index.is_some() {
345 // Assuming it's not empty, query component is from
346 // the identified '?' to the remainder of the `pk11_uri`:
347 if let Some(pk11_query) = pk11_uri
348 .get(query_component_index.unwrap() + 1..)
349 .filter(|pk11_query| !pk11_query.is_empty())
350 {
351 pk11_query
352 .split('&')
353 .enumerate()
354 .try_for_each(|(count, pk11_qattr)| {
355 pk11_qattr::assign(pk11_qattr, &mut mapping).map_err(|validation_err| {
356 let tidy_pk11_uri = tidy(pk11_uri);
357 let tidy_pk11_query = tidy(pk11_query);
358 let tidy_pk11_qattr = tidy(pk11_qattr);
359
360 let mut violation = validation_err.violation;
361 let mut help = validation_err.help;
362
363 let error_start = if !tidy_pk11_qattr.is_empty() {
364 tidy_pk11_query.find(&tidy_pk11_qattr).unwrap()
365 } else {
366 // assign this here rather than adding O(n) runtime checks
367 // for basically an unlikely outlier type of error:
368 violation = String::from("Misplaced query delimiter.");
369 help = String::from("Remove the misplaced '&' delimiter.");
370 find_empty_attr_index(&tidy_pk11_query, count, '&')
371 } + tidy_pk11_uri.find('?').unwrap()
372 + 1;
373 PK11URIError {
374 pk11_uri: tidy_pk11_uri,
375 error_span: (error_start, error_start + tidy_pk11_qattr.len()),
376 violation,
377 help,
378 }
379 })
380 })?;
381 }
382
383 // "...semantics of using both attributes in the same URI string is implementation specific
384 // but such use SHOULD be avoided. Attribute "module-name" is preferred to "module-path" due
385 // to its system-independent nature, but the latter may be more suitable for development and debugging."
386 #[cfg(all(debug_assertions, feature = "debug_warnings"))]
387 if mapping.module_name.is_some() && mapping.module_path.is_some() {
388 println!(
389 "pkcs11 warning: using both `module-name` and `module-path` SHOULD be avoided. \
390 Attribute `module-name` is preferred due to its system-independent nature."
391 );
392 }
393
394 // "If a URI contains both "pin-source" and "pin-value" query attributes, the URI SHOULD be refused as invalid."
395 #[cfg(all(debug_assertions, feature = "debug_warnings"))]
396 if mapping.pin_source.is_some() && mapping.pin_value.is_some() {
397 println!(
398 r#"pkcs11 warning: a PKCS#11 URI containing both "pin-source" and "pin-value" query attributes SHOULD be refused as invalid."#
399 );
400 }
401 }
402
403 Ok(mapping)
404}
405
406/// Helper function to identify the location of an empty path|query component.
407/// An empty component is a phenomena of a superfluous ';' or '&' delimiter such
408/// as `pkcs11:foo=bar;`
409/// ^ trailing ';' is a RFC7512 violation.
410fn find_empty_attr_index(tidy_attr: &str, split_count: usize, delimiter: char) -> usize {
411 tidy_attr
412 .match_indices(delimiter)
413 .nth(split_count)
414 .unwrap_or((tidy_attr.len() - 1, "_"))
415 .0
416}
417
418/// Establish the basis for reliable error reporting by removing '\n' newline
419/// and '\t' tab formatting.
420fn tidy(maybe_messy: &str) -> String {
421 maybe_messy.replace(['\n', '\t'], "")
422}