Skip to content

Commit f0eabc5

Browse files
committed
Add support for PKCS#11 token key management
1 parent 1412f3a commit f0eabc5

4 files changed

Lines changed: 234 additions & 2 deletions

File tree

bindings/bindings.toml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,9 @@ types = [
138138
"HpkeAeadId",
139139
"HpkeKdfId",
140140
"HpkeKemId",
141+
"PK11AttrFlags",
142+
"PK11SlotListStr",
143+
"PK11SlotListElementStr",
141144
"PK11SymKeyStr",
142145
"SECKEYPrivateKeyStr",
143146
]
@@ -147,6 +150,7 @@ functions = [
147150
"CERT_GetCertificateDer",
148151
"NSS_SetAlgorithmPolicy",
149152
"PK11_AEADOp",
153+
"PK11_Authenticate",
150154
"PK11_CipherOp",
151155
"PK11_CreateContextBySymKey",
152156
"PK11_Decrypt",
@@ -160,14 +164,20 @@ functions = [
160164
"PK11_FindCertFromNickname",
161165
"PK11_FindKeyByAnyCert",
162166
"PK11_FreeSlot",
167+
"PK11_FreeSlotList",
163168
"PK11_FreeSymKey",
164169
"PK11_GenerateKeyPairWithOpFlags",
170+
"PK11_GetNextSymKey",
171+
"PK11_GetSymKeyNickname",
165172
"PK11_GenerateKeyPair",
166173
"PK11_GenerateRandom",
174+
"PK11_GetAllTokens",
167175
"PK11_GetBlockSize",
176+
"PK11_GetInternalKeySlot",
168177
"PK11_GetInternalSlot",
169178
"PK11_GetKeyData",
170179
"PK11_GetMechanism",
180+
"PK11_GetTokenName",
171181
"PK11_HashBuf",
172182
"PK11_HPKE_Deserialize",
173183
"PK11_HPKE_ExportSecret",
@@ -184,10 +194,14 @@ functions = [
184194
"PK11_ImportDERPrivateKeyInfoAndReturnKey",
185195
"PK11_ImportPublicKey",
186196
"PK11_ImportSymKey",
197+
"PK11_ListFixedKeysInSlot",
187198
"PK11_PubDeriveWithKDF",
188199
"PK11_ReadRawAttribute",
200+
"PK11_ReferenceSlot",
189201
"PK11_ReferenceSymKey",
202+
"PK11_SetSymKeyNickname",
190203
"PK11_SignWithMechanism",
204+
"PK11_TokenKeyGenWithFlags",
191205
"PK11_VerifyWithMechanism",
192206
"PK11_WrapPrivKey",
193207
"SECITEM_AllocItem",
@@ -217,6 +231,7 @@ variables = [
217231
"NSS_USE_ALG_IN_SSL_KX",
218232
"PK11_ATTR_INSENSITIVE",
219233
"PK11_ATTR_PRIVATE",
234+
"PK11_ATTR_TOKEN",
220235
"PK11_ATTR_PUBLIC",
221236
"PK11_ATTR_SENSITIVE",
222237
"PK11_ATTR_SESSION",
@@ -236,7 +251,9 @@ variables = [
236251
"CKG_NO_GENERATE",
237252
"CKM_AES_GCM",
238253
"CKM_CHACHA20_POLY1305",
254+
"CKF_DECRYPT",
239255
"CKF_DERIVE",
256+
"CKF_ENCRYPT",
240257
"CKM_EC_KEY_PAIR_GEN",
241258
"CK_INVALID_HANDLE",
242259
"CKF_HKDF_SALT_NULL",
@@ -249,6 +266,7 @@ variables = [
249266
"CKM_SHA256_HMAC",
250267
"CKM_SHA384_HMAC",
251268
"CKM_SHA512_HMAC",
269+
"CKM_AES_KEY_GEN",
252270
"CKM_ECDSA",
253271
"CKM_EDDSA",
254272
]

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ pub mod ec;
2828
pub mod hash;
2929
pub mod hmac;
3030
pub mod p11;
31+
pub mod pk11_utils;
3132
mod prio;
3233
mod replay;
3334
mod secrets;

src/p11.rs

Lines changed: 92 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ use std::{
1515
cell::RefCell,
1616
convert::TryFrom as _,
1717
fmt::{self, Debug, Formatter},
18-
os::raw::c_uint,
18+
os::raw::{c_int, c_uint},
1919
ptr::null_mut,
2020
};
2121

@@ -174,6 +174,96 @@ impl Slot {
174174
pub fn internal() -> Res<Self> {
175175
unsafe { Self::from_ptr(PK11_GetInternalSlot()) }
176176
}
177+
178+
pub fn internal_key_slot() -> Res<Self> {
179+
unsafe { Self::from_ptr(PK11_GetInternalKeySlot()) }
180+
}
181+
182+
#[must_use]
183+
pub fn token_name(&self) -> String {
184+
let name = unsafe { PK11_GetTokenName(self.ptr) };
185+
if name.is_null() {
186+
return String::new();
187+
}
188+
unsafe { std::ffi::CStr::from_ptr(name) }
189+
.to_string_lossy()
190+
.into_owned()
191+
}
192+
193+
pub fn authenticate(&self) -> Res<()> {
194+
secstatus_to_res(unsafe { PK11_Authenticate(self.ptr, PRBool::from(true), null_mut()) })
195+
}
196+
197+
/// Find a persistent symmetric key on this slot by nickname.
198+
/// Returns `None` if no key with the given nickname exists.
199+
#[must_use]
200+
pub fn find_key_by_nickname(&self, nickname: &str) -> Option<SymKey> {
201+
let c_nickname = std::ffi::CString::new(nickname).ok()?;
202+
let ptr = unsafe {
203+
PK11_ListFixedKeysInSlot(self.ptr, c_nickname.as_ptr().cast_mut(), null_mut())
204+
};
205+
if ptr.is_null() {
206+
None
207+
} else {
208+
SymKey::from_ptr(ptr).ok()
209+
}
210+
}
211+
212+
/// Generate a persistent symmetric key on this slot with a nickname.
213+
pub fn generate_token_key(
214+
&self,
215+
mechanism: CK_MECHANISM_TYPE,
216+
key_size: usize,
217+
nickname: &str,
218+
) -> Res<SymKey> {
219+
let key = unsafe {
220+
SymKey::from_ptr(PK11_TokenKeyGenWithFlags(
221+
self.ptr,
222+
mechanism,
223+
null_mut(),
224+
c_int::try_from(key_size).map_err(|_| Error::IntegerOverflow)?,
225+
null_mut(),
226+
CK_FLAGS::from(CKF_ENCRYPT | CKF_DECRYPT),
227+
PK11AttrFlags::from(PK11_ATTR_TOKEN | PK11_ATTR_PRIVATE | PK11_ATTR_SENSITIVE),
228+
null_mut(),
229+
))
230+
}?;
231+
let c_nickname = std::ffi::CString::new(nickname).map_err(|_| Error::InvalidInput)?;
232+
secstatus_to_res(unsafe { PK11_SetSymKeyNickname(*key, c_nickname.as_ptr()) })?;
233+
Ok(key)
234+
}
235+
}
236+
237+
/// Returns all available token slots for the given mechanism.
238+
#[must_use]
239+
pub fn all_token_slots(mechanism: CK_MECHANISM_TYPE) -> Vec<Slot> {
240+
let list = unsafe {
241+
PK11_GetAllTokens(
242+
mechanism,
243+
PRBool::from(false),
244+
PRBool::from(false),
245+
null_mut(),
246+
)
247+
};
248+
if list.is_null() {
249+
return Vec::new();
250+
}
251+
let mut result = Vec::new();
252+
unsafe {
253+
let mut elem = (*list).head;
254+
while !elem.is_null() {
255+
let slot_ptr = (*elem).slot;
256+
if !slot_ptr.is_null() {
257+
PK11_ReferenceSlot(slot_ptr);
258+
if let Ok(slot) = Slot::from_ptr(slot_ptr) {
259+
result.push(slot);
260+
}
261+
}
262+
elem = (*elem).next;
263+
}
264+
PK11_FreeSlotList(list);
265+
}
266+
result
177267
}
178268

179269
// Note: PK11SymKey is internally reference counted
@@ -249,7 +339,7 @@ pub fn randomize<B: AsMut<[u8]>>(mut buf: B) -> B {
249339
#[cfg(not(feature = "disable-random"))]
250340
pub fn randomize<B: AsMut<[u8]>>(mut buf: B) -> B {
251341
let m_buf = buf.as_mut();
252-
let len = std::os::raw::c_int::try_from(m_buf.len()).expect("usize fits into c_int");
342+
let len = c_int::try_from(m_buf.len()).expect("usize fits into c_int");
253343
secstatus_to_res(unsafe { PK11_GenerateRandom(m_buf.as_mut_ptr(), len) }).expect("NSS failed");
254344
buf
255345
}

src/pk11_utils.rs

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
2+
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
3+
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
4+
// option. This file may not be copied, modified, or distributed
5+
// except according to those terms.
6+
7+
use crate::err::{Error, Res};
8+
9+
#[derive(Debug, Clone, PartialEq, Eq)]
10+
pub struct Pkcs11Uri {
11+
pub token: Option<String>,
12+
}
13+
14+
fn percent_decode(s: &str) -> String {
15+
let mut result = Vec::with_capacity(s.len());
16+
let bytes = s.as_bytes();
17+
let mut i = 0;
18+
while i < bytes.len() {
19+
if bytes[i] == b'%'
20+
&& i + 2 < bytes.len()
21+
&& let Ok(byte) =
22+
u8::from_str_radix(std::str::from_utf8(&bytes[i + 1..i + 3]).unwrap_or(""), 16)
23+
{
24+
result.push(byte);
25+
i += 3;
26+
continue;
27+
}
28+
result.push(bytes[i]);
29+
i += 1;
30+
}
31+
String::from_utf8_lossy(&result).into_owned()
32+
}
33+
34+
fn percent_encode(s: &str) -> String {
35+
let mut result = String::with_capacity(s.len());
36+
for b in s.bytes() {
37+
match b {
38+
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'.' | b'_' | b'~' => {
39+
result.push(b as char);
40+
}
41+
_ => {
42+
use std::fmt::Write as _;
43+
write!(result, "%{b:02X}").expect("write to String");
44+
}
45+
}
46+
}
47+
result
48+
}
49+
50+
/// Parse a PKCS#11 URI (RFC 7512).
51+
/// Expects the input to start with "pkcs11:".
52+
pub fn parse(uri: &str) -> Res<Pkcs11Uri> {
53+
let path = uri.strip_prefix("pkcs11:").ok_or(Error::InvalidInput)?;
54+
55+
let mut token = None;
56+
for attr in path.split(';') {
57+
if let Some((key, value)) = attr.split_once('=')
58+
&& key == "token"
59+
{
60+
token = Some(percent_decode(value));
61+
}
62+
}
63+
64+
Ok(Pkcs11Uri { token })
65+
}
66+
67+
/// Build a PKCS#11 URI from a token name.
68+
#[must_use]
69+
pub fn build(token_name: &str) -> String {
70+
format!("pkcs11:token={}", percent_encode(token_name))
71+
}
72+
73+
#[cfg(test)]
74+
mod tests {
75+
use test_fixture::fixture_init;
76+
77+
use super::*;
78+
79+
#[test]
80+
fn parse_simple() {
81+
fixture_init();
82+
let uri = parse("pkcs11:token=NSS%20Certificate%20DB").unwrap();
83+
assert_eq!(uri.token.as_deref(), Some("NSS Certificate DB"));
84+
}
85+
86+
#[test]
87+
fn parse_no_token() {
88+
fixture_init();
89+
let uri = parse("pkcs11:manufacturer=Mozilla").unwrap();
90+
assert_eq!(uri.token, None);
91+
}
92+
93+
#[test]
94+
fn parse_multiple_attrs() {
95+
fixture_init();
96+
let uri = parse("pkcs11:token=MyToken;manufacturer=Test;serial=1234").unwrap();
97+
assert_eq!(uri.token.as_deref(), Some("MyToken"));
98+
}
99+
100+
#[test]
101+
fn parse_not_pkcs11() {
102+
fixture_init();
103+
assert!(parse("http://example.com").is_err());
104+
}
105+
106+
#[test]
107+
fn build_uri() {
108+
fixture_init();
109+
assert_eq!(
110+
build("NSS Certificate DB"),
111+
"pkcs11:token=NSS%20Certificate%20DB"
112+
);
113+
}
114+
115+
#[test]
116+
fn roundtrip() {
117+
fixture_init();
118+
let name = "My Token (Test-v2)";
119+
let uri_str = build(name);
120+
let parsed = parse(&uri_str).unwrap();
121+
assert_eq!(parsed.token.as_deref(), Some(name));
122+
}
123+
}

0 commit comments

Comments
 (0)