#[derive(Clone, Default)]
pub struct Pkcs11UriAttributes {
pub token: Option<String>,
pub object: Option<String>,
pub id: Option<Vec<u8>>,
pub(crate) pin_value: Option<String>,
pub module_path: Option<String>,
}
impl Pkcs11UriAttributes {
pub fn pin_value(&self) -> Option<&str> {
self.pin_value.as_deref()
}
pub fn has_pin(&self) -> bool {
self.pin_value.is_some()
}
pub fn set_pin_value(&mut self, pin: Option<String>) {
self.pin_value = pin;
}
}
impl std::fmt::Debug for Pkcs11UriAttributes {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Pkcs11UriAttributes")
.field("token", &self.token)
.field("object", &self.object)
.field("id", &self.id)
.field("pin_value", &self.pin_value.as_ref().map(|_| "***"))
.field("module_path", &self.module_path)
.finish()
}
}
#[derive(Clone)]
pub struct Pkcs11Uri {
pub raw: String,
pub attrs: Pkcs11UriAttributes,
}
impl std::fmt::Debug for Pkcs11Uri {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let display = redact_raw_uri(&self.raw);
f.debug_struct("Pkcs11Uri")
.field("raw", &display)
.field("attrs", &self.attrs)
.finish()
}
}
pub(crate) fn redact_raw_uri(uri: &str) -> String {
if let Some((before, after)) = uri.split_once("pin-value=") {
let rest = after.find('&').map_or("", |i| &after[i..]);
format!("{before}pin-value=***{rest}")
} else {
uri.to_owned()
}
}
impl Pkcs11Uri {
pub fn parse(raw: impl Into<String>) -> Option<Self> {
let raw = raw.into();
let attrs = parse_attrs(&raw)?;
Some(Self { raw, attrs })
}
}
pub(crate) fn parse_attrs(uri: &str) -> Option<Pkcs11UriAttributes> {
let body = uri.strip_prefix("pkcs11:")?;
let (path_part, query_part) = body.split_once('?').unwrap_or((body, ""));
let mut attrs = Pkcs11UriAttributes::default();
for segment in path_part.split(';').filter(|s| !s.is_empty()) {
if let Some((k, v)) = segment.split_once('=') {
match k {
"token" => attrs.token = Some(pct_decode(v)),
"object" => attrs.object = Some(pct_decode(v)),
"id" => attrs.id = Some(decode_id(v)),
"module-path" => attrs.module_path = Some(pct_decode(v)),
_ => {}
}
}
}
for segment in query_part.split('&').filter(|s| !s.is_empty()) {
if let Some((k, v)) = segment.split_once('=') {
if k == "pin-value" {
attrs.pin_value = Some(pct_decode(v));
}
}
}
Some(attrs)
}
fn pct_decode(s: &str) -> String {
let bytes = s.as_bytes();
let mut out = Vec::with_capacity(s.len());
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'%' && i + 2 < bytes.len() {
if let (Some(hi), Some(lo)) = (hex_nibble(bytes[i + 1]), hex_nibble(bytes[i + 2])) {
out.push((hi << 4) | lo);
i += 3;
continue;
}
}
out.push(bytes[i]);
i += 1;
}
String::from_utf8_lossy(&out).into_owned()
}
fn decode_id(s: &str) -> Vec<u8> {
let bytes = s.as_bytes();
let mut out = Vec::new();
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'%' && i + 2 < bytes.len() {
if let (Some(hi), Some(lo)) = (hex_nibble(bytes[i + 1]), hex_nibble(bytes[i + 2])) {
out.push((hi << 4) | lo);
i += 3;
continue;
}
}
out.push(bytes[i]);
i += 1;
}
out
}
fn hex_nibble(b: u8) -> Option<u8> {
match b {
b'0'..=b'9' => Some(b - b'0'),
b'a'..=b'f' => Some(b - b'a' + 10),
b'A'..=b'F' => Some(b - b'A' + 10),
_ => None,
}
}
pub fn pct_encode_path(s: &str) -> String {
const HEX: &[u8; 16] = b"0123456789ABCDEF";
let mut out = String::with_capacity(s.len());
for b in s.bytes() {
if b.is_ascii_alphanumeric() || matches!(b, b'-' | b'.' | b'_' | b'~') {
out.push(b as char);
} else {
out.push('%');
out.push(HEX[(b >> 4) as usize] as char);
out.push(HEX[(b & 0xf) as usize] as char);
}
}
out
}
pub fn merge_object_label(uri: &str, label: &str) -> String {
let body = uri.strip_prefix("pkcs11:").unwrap_or(uri);
let (path, query) = body.split_once('?').unwrap_or((body, ""));
let mut segments: Vec<&str> = path
.split(';')
.filter(|s| !s.starts_with("object="))
.collect();
let encoded = pct_encode_path(label);
let label_seg = format!("object={encoded}");
segments.push(&label_seg);
let new_path = segments.join(";");
if query.is_empty() {
format!("pkcs11:{new_path}")
} else {
format!("pkcs11:{new_path}?{query}")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rejects_non_pkcs11_uri() {
assert!(Pkcs11Uri::parse("file:///tmp/key.pem").is_none());
assert!(Pkcs11Uri::parse("").is_none());
}
#[test]
fn minimal_uri_empty_attributes() {
let p = Pkcs11Uri::parse("pkcs11:").unwrap();
assert!(p.attrs.token.is_none());
assert!(p.attrs.object.is_none());
assert_eq!(p.raw, "pkcs11:");
}
#[test]
fn parses_token_and_object() {
let p = Pkcs11Uri::parse("pkcs11:token=MyToken;object=cakey;type=private").unwrap();
assert_eq!(p.attrs.token.as_deref(), Some("MyToken"));
assert_eq!(p.attrs.object.as_deref(), Some("cakey"));
assert!(p.raw.starts_with("pkcs11:"));
}
#[test]
fn parses_pin_value_query() {
let p = Pkcs11Uri::parse("pkcs11:token=T;object=k;type=private?pin-value=12345").unwrap();
assert_eq!(p.attrs.pin_value(), Some("12345"));
assert!(p.attrs.has_pin());
}
#[test]
fn parses_percent_encoded_token() {
let p = Pkcs11Uri::parse("pkcs11:token=My%20Token;object=key").unwrap();
assert_eq!(p.attrs.token.as_deref(), Some("My Token"));
}
#[test]
fn parses_id_bytes() {
let p = Pkcs11Uri::parse("pkcs11:token=T;object=k;id=%01%02%03").unwrap();
assert_eq!(p.attrs.id.as_deref(), Some(&[0x01u8, 0x02, 0x03][..]));
}
#[test]
fn ignores_unknown_attributes() {
let p =
Pkcs11Uri::parse("pkcs11:token=T;object=k;type=private;manufacturer=OpenSC").unwrap();
assert_eq!(p.attrs.token.as_deref(), Some("T"));
assert_eq!(p.attrs.object.as_deref(), Some("k"));
}
}