1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
use std::{collections::BTreeMap, str::FromStr};
#[allow(unused_imports)]
use log::{debug, error, info, trace, warn};
use bytes::Bytes;
use x509_certificate::{signing::InMemorySigningKeyPair, Signer};
use cryptoki::{
context::{CInitializeArgs, Pkcs11},
mechanism::{self, Mechanism},
object::{Attribute, AttributeType, ObjectClass, ObjectHandle},
session::{Session, UserType},
slot::Slot,
types::AuthPin,
};
use ring::digest;
use der::{asn1, Encode};
use crate::security::{
authentication::authentication_builtin::types::CertificateAlgorithm,
config::{parse_config_error, to_config_error_parse, ConfigError},
types::{security_error, SecurityResult},
};
/// PrivateKey is the private key associaed with a Certificate (see neghbouring
/// module)
///
/// It can live within a .pem file (and normal memory), or in a Hardware
/// Security Module.
#[derive(Debug)]
pub(crate) enum PrivateKey {
InMemory {
priv_key: InMemorySigningKeyPair,
},
InHSM {
key_algorithm: CertificateAlgorithm,
// We store context and slot just in case dropping them would close them.
#[allow(dead_code)]
context: Pkcs11,
#[allow(dead_code)]
slot: Slot,
session: Session,
key_object_handle: ObjectHandle,
},
}
// TODO: decrypt a password protected key
impl PrivateKey {
pub fn from_pem(pem_data: impl AsRef<[u8]>) -> Result<Self, ConfigError> {
let priv_key = InMemorySigningKeyPair::from_pkcs8_pem(pem_data.as_ref())
.map_err(to_config_error_parse("Private key parse error"))?;
Ok(PrivateKey::InMemory { priv_key })
}
// Process:
//
// decode object label in path
// load HSM library
// initialize library
// get slots and enumerate
// find slot where token is present and matches object label
// open RO session to token and login with PIN
// find private key objects
// check that private key object supports "sign"
// store library handle, slot handle, session handle to PrivateKey object
// store object handle to PrivateKey
// return PrivateKey
pub fn from_pkcs11_uri_path_and_query(
path_and_query: &str,
key_algorithm: CertificateAlgorithm,
) -> Result<Self, ConfigError> {
let interesting_attributes = vec![
AttributeType::AllowedMechanisms,
AttributeType::Class,
AttributeType::Label,
AttributeType::KeyType,
AttributeType::Sign,
AttributeType::Sensitive,
];
// decode URI: path and possible query (PIN)
//
// example path_and_query, which is part of URI after scheme "pkcs11:"
//
// token=my_token_label?pin-value=1234&module-path=/usr/lib/softhsm/libsofthsm2.
// so
//
// The "module-path" is a query-attribute like the PIN, and not a path-attribute
// like "token", which may seem a bit strange choice, but that is what RFC
// 7512 says.
let (path, query) = path_and_query
.split_once('?')
.unwrap_or((path_and_query, ""));
let path_attrs: BTreeMap<&str, &str> =
path.split(';').filter_map(|a| a.split_once('=')).collect();
let query_attrs: BTreeMap<&str, &str> =
query.split('&').filter_map(|a| a.split_once('=')).collect();
let token_label = path_attrs.get("object").ok_or(parse_config_error(
"pkcs11 URI must specify attribute \"object\" i.e. token label".to_string(),
))?;
let pin_value_opt = query_attrs.get("pin-value");
let module_path: &str = query_attrs
.get("module-path")
.cloned()
.unwrap_or("/usr/lib/softhsm/libsofthsm2.so"); // Some semi-reasnoable default value.
info!("Opening PKCS#11 HSM client library {module_path}");
let context = Pkcs11::new(module_path)?;
context.initialize(CInitializeArgs::OsThreads)?;
debug!("PKCS#11 HSM library info: {:?}", context.get_library_info());
let slots = context.get_all_slots()?;
debug!("PKCS#11 HSM found {} slots.", slots.len());
for (num, slot) in slots.iter().enumerate() {
let slot_info = context.get_slot_info(*slot);
match slot_info {
Ok(si) if si.token_present() => {
match context.get_token_info(*slot) {
Ok(token_info) => {
if token_info.label() == *token_label {
info!("Found matching token \"{token_label}\" in slot {num}.");
let session = context.open_ro_session(*slot)?;
let secret_pin_opt: Option<AuthPin> =
pin_value_opt.map(|p| AuthPin::from_str(p).unwrap());
// unwrap is safe, because error type is Infallible
session.login(UserType::User, secret_pin_opt.as_ref())?; // bail on failure
info!(
"Logged into token \"{}\" , using PIN = {:?}",
token_label,
secret_pin_opt.is_some()
);
// Now ,iterate though objects in the token.
for (obj_num, obj) in session.find_objects(&[])?.iter().enumerate() {
let attr = session.get_attributes(*obj, &interesting_attributes)?;
// Check the attributes. Does this look like a private key?
if attr
.iter()
.any(|a| a == &Attribute::Class(ObjectClass::PRIVATE_KEY))
&& attr.iter().any(|a| a == &Attribute::Sign(true))
{
// Is a private key and declares to support "sign" operation.
let object_label = attr.iter().find_map(|a| match a {
Attribute::Label(bytes) => Some(String::from_utf8_lossy(bytes)),
_ => None,
});
info!("Object {obj_num}: Found a Private Key. label={object_label:?}");
// Test that the signing operation works.
let test_signature_result =
session.sign(&key_algorithm.into(), *obj, b"This is just dummy data");
if test_signature_result.is_ok() {
debug!("Object {obj_num}: Test signing success.");
// Object looks like a legit private key, so we'll use that.
return Ok(PrivateKey::InHSM {
key_algorithm,
context,
slot: *slot,
session,
key_object_handle: *obj,
});
} else {
warn!(
"Object {obj_num}: Test signing fails: {test_signature_result:?}. Cannot \
use this as private key."
);
}
} else {
debug!("Object {obj_num}: Attributes do not match {attr:?}");
}
} // for objects
} else {
debug!(
"Slot {}, token label={}: This is not the token we are looking for.",
num,
token_info.label()
);
}
}
Err(e) => warn!("Slot {num}: get_token_info() fails: {e:?}"),
}
} // Ok with token_present
Ok(_) => info!("Slot {num} has no token"),
Err(e) => warn!("Slot {num} get_slot_info() error: {e:?}"),
}
} // for slots
Err(parse_config_error(
"Did not find a suitable private key in the HSM".to_string(),
))
}
pub fn sign(&self, msg: &[u8]) -> SecurityResult<Bytes> {
match self {
PrivateKey::InMemory { priv_key } => priv_key
.try_sign(msg)
.map(|s| Bytes::copy_from_slice(s.as_ref()))
.map_err(|e| security_error(&format!("Signing failure: {e:?}"))),
PrivateKey::InHSM {
key_algorithm,
session,
key_object_handle,
..
} => {
// DDS Security uses ASN.1-encoded ECDSA-SHA256 signatures.
//
// PKCS#11 (HSM) provides fixed-length ECDSA-signatures without SHA256.
//
// In order to
// use HSM signing in DDS, we must first compute the SHA256 digest, then sign
// using ECDSA. The result is two 32-byte integers (r,s) concatenated
// together. These need to be ASN.1 DER-encoded according to RFC 3279
// Section 2.2.3 as
// ```
// Ecdsa-Sig-Value ::= SEQUENCE {
// r INTEGER,
// s INTEGER }
// ```
//
// Thanks to the
// [ring crate documentation](https://docs.rs/ring/0.17.8/ring/signature/index.html)
// for explaining this.
// First, hash the message to be signed. Then sign the hash, not the message.
let msg_digest = digest::digest(&digest::SHA256, msg);
// Second, ask HSM to compute the signature
let sign_mechanism = Mechanism::from(*key_algorithm);
let hsm_signature_raw =
session.sign(&sign_mechanism, *key_object_handle, msg_digest.as_ref())?;
// Sanity check.
if hsm_signature_raw.len() != 64 {
return Err(security_error(&format!(
"Expected signature len=64, got len={}",
hsm_signature_raw.len()
)));
}
// Third, convert raw signature to ASN.1 with DER
let (r, s) = hsm_signature_raw.split_at(32); // safe, because of the length check above
let mut hsm_signature_der = Vec::with_capacity(80); // typically needs about 70..72 bytes
// Safety: We expect all the .unwrap() calls below to succeed, becaues the
// possible errors are size overflows, and here the input sizes are fixed.
let sequence_of_r_s = vec![
asn1::UintRef::new(r).unwrap(),
asn1::UintRef::new(s).unwrap(),
];
sequence_of_r_s.encode(&mut hsm_signature_der).unwrap();
Ok(Bytes::from(hsm_signature_der))
}
}
} // fn
}
// Map our internal choice of certificate key algoritm to cryptoki Mechanism
impl From<CertificateAlgorithm> for Mechanism<'_> {
fn from(value: CertificateAlgorithm) -> Self {
match value {
CertificateAlgorithm::ECPrime256v1 => Mechanism::Ecdsa,
// DDS Security spec v1.1 Section "9.3.2.5.2 HandshakeReplyMessageToken" says
//
// "If the Participant Private Key is a RSA key, then [...]
// The digital signature shall be computed using the RSASSA-PSS
// algorithm specified in PKCS #1 (IETF 3447) RSA Cryptography
// Specifications Version 2.1 [44], using SHA256 as hash function, and
// MGF1 with SHA256 (mgf1sha256) as mask generation function."
//
// TODO: Make sure this is correctly mapped to `PkcsPssParams`
CertificateAlgorithm::RSA2048 => Mechanism::Sha256RsaPkcsPss(mechanism::rsa::PkcsPssParams {
hash_alg: mechanism::MechanismType::SHA256, // is this correct?
mgf: mechanism::rsa::PkcsMgfType::MGF1_SHA256,
s_len: 32.into(), /* is this correct?
* s_len is documented as
* "length, in bytes, of the salt value used in the PSS encoding;
* typical values are the length of the message hash and zero" */
}),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
pub fn parse_private_key() {
let priv_key_pem = r#"-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgaPBddesE0rHiP5/c
+djUjctfNoMAa5tNxOdged9AQtOhRANCAATKbyUP/dWap5kUbXky9qmhBc9ne0hg
EEJPTyIYWQldbZS8GH/4SOMViyWhL/BcSU48V0RidVPvyKqqiLyVaVmk
-----END PRIVATE KEY-----
"#;
let key = PrivateKey::from_pem(priv_key_pem).unwrap();
println!("{key:?}");
}
}