Skip to content

Commit 8f75686

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

4 files changed

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

179276
// Note: PK11SymKey is internally reference counted
@@ -249,7 +346,7 @@ pub fn randomize<B: AsMut<[u8]>>(mut buf: B) -> B {
249346
#[cfg(not(feature = "disable-random"))]
250347
pub fn randomize<B: AsMut<[u8]>>(mut buf: B) -> B {
251348
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");
349+
let len = c_int::try_from(m_buf.len()).expect("usize fits into c_int");
253350
secstatus_to_res(unsafe { PK11_GenerateRandom(m_buf.as_mut_ptr(), len) }).expect("NSS failed");
254351
buf
255352
}

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'%' && i + 2 < bytes.len() {
20+
if let Ok(byte) = u8::from_str_radix(
21+
std::str::from_utf8(&bytes[i + 1..i + 3]).unwrap_or(""),
22+
16,
23+
) {
24+
result.push(byte);
25+
i += 3;
26+
continue;
27+
}
28+
}
29+
result.push(bytes[i]);
30+
i += 1;
31+
}
32+
String::from_utf8_lossy(&result).into_owned()
33+
}
34+
35+
fn percent_encode(s: &str) -> String {
36+
let mut result = String::with_capacity(s.len());
37+
for b in s.bytes() {
38+
match b {
39+
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'.' | b'_' | b'~' => {
40+
result.push(b as char);
41+
}
42+
_ => {
43+
result.push_str(&format!("%{:02X}", b));
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
54+
.strip_prefix("pkcs11:")
55+
.ok_or(Error::InvalidInput)?;
56+
57+
let mut token = None;
58+
for attr in path.split(';') {
59+
if let Some((key, value)) = attr.split_once('=') {
60+
if key == "token" {
61+
token = Some(percent_decode(value));
62+
}
63+
}
64+
}
65+
66+
Ok(Pkcs11Uri { token })
67+
}
68+
69+
/// Build a PKCS#11 URI from a token name.
70+
pub fn build(token_name: &str) -> String {
71+
format!("pkcs11:token={}", percent_encode(token_name))
72+
}
73+
74+
#[cfg(test)]
75+
mod tests {
76+
use super::*;
77+
use test_fixture::fixture_init;
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)