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}