Skip to content

Commit 2b0e41f

Browse files
authored
Merge pull request #3677 from Bravo555/feat/cert-renew-pkcs11
feat: `tedge cert renew c8y` using HSM
2 parents 7aa1354 + b5222c8 commit 2b0e41f

File tree

15 files changed

+396
-113
lines changed

15 files changed

+396
-113
lines changed

crates/common/certificate/src/lib.rs

Lines changed: 124 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use anyhow::Context;
2+
use camino::Utf8Path;
13
use device_id::DeviceIdError;
24
use rcgen::Certificate;
35
use rcgen::CertificateParams;
@@ -6,8 +8,11 @@ use sha1::Digest;
68
use sha1::Sha1;
79
use std::path::Path;
810
use std::path::PathBuf;
11+
use tedge_p11_server::CryptokiConfig;
912
use time::Duration;
1013
use time::OffsetDateTime;
14+
use x509_parser::oid_registry;
15+
use x509_parser::public_key::PublicKey;
1116
pub use zeroize::Zeroizing;
1217
#[cfg(feature = "reqwest")]
1318
mod cloud_root_certificate;
@@ -140,11 +145,110 @@ pub enum ValidityStatus {
140145
NotValidYet { valid_in: std::time::Duration },
141146
}
142147

148+
#[derive(Debug, Clone)]
143149
pub enum KeyKind {
144150
/// Create a new key
145151
New,
146152
/// Reuse the existing PEM-encoded key pair
147153
Reuse { keypair_pem: String },
154+
/// Reuse the keypair where we can't read the private key because it's on the HSM.
155+
ReuseRemote(RemoteKeyPair),
156+
}
157+
158+
impl KeyKind {
159+
pub fn from_cryptoki_and_existing_cert(
160+
cryptoki_config: CryptokiConfig,
161+
current_cert: &Utf8Path,
162+
) -> Result<Self, CertificateError> {
163+
let cert = PemCertificate::from_pem_file(current_cert)?;
164+
let cert = PemCertificate::extract_certificate(&cert.pem)?;
165+
let public_key_raw = cert.public_key().subject_public_key.data.to_vec();
166+
167+
// map public key to signature identifier (some keys support many types of signatures, on
168+
// our side we only do P256/P384/RSA2048 with a single type of signature each)
169+
// for P256/P384, former has only SHA256 and latter only SHA384 signature, so no questions there
170+
// but for RSA, AFAIK we can use SHA256 with all of them. RSA_PSS also isn't supported by
171+
// rcgen, but we're free to use regular RSA_PKCS1_SHA256
172+
let public_key = cert
173+
.public_key()
174+
.parsed()
175+
.context("Failed to read public key from the certificate")?;
176+
let signature_algorithm = match public_key {
177+
PublicKey::EC(ec) => match ec.key_size() {
178+
256 => oid_registry::OID_SIG_ECDSA_WITH_SHA256,
179+
384 => oid_registry::OID_SIG_ECDSA_WITH_SHA384,
180+
// P521 (size 528 reported by key_size() is not yet supported by rcgen)
181+
// https://github.com/rustls/rcgen/issues/60
182+
_ => {
183+
return Err(anyhow::anyhow!("Unsupported public key. Only P256/P384/RSA2048/RSA3072/RSA4096 are supported for certificate renewal").into());
184+
}
185+
},
186+
PublicKey::RSA(_) => oid_registry::OID_PKCS1_SHA256WITHRSA,
187+
_ => return Err(anyhow::anyhow!("Unsupported public key. Only P256/P384/RSA2048/RSA3072/RSA4096 are supported for certificate renewal").into())
188+
};
189+
let signature_algorithm: Vec<u64> = signature_algorithm
190+
.iter()
191+
.map(|i| i.collect())
192+
.unwrap_or_default();
193+
let algorithm = rcgen::SignatureAlgorithm::from_oid(&signature_algorithm)?;
194+
195+
Ok(Self::ReuseRemote(RemoteKeyPair {
196+
cryptoki_config,
197+
public_key_raw,
198+
algorithm,
199+
}))
200+
}
201+
}
202+
203+
/// A key pair using a remote private key.
204+
///
205+
/// To generate a CSR we need:
206+
/// - the public key, because the public key is a part of the certificate (subject public key info)
207+
/// - the private key, to sign the CSR to prove that the public key is ours
208+
///
209+
/// With private key in the HSM, we can't access its private parts, but we can still use it to sign.
210+
/// For the public key, instead of deriving it from the private key, which needs some additions to
211+
/// our PKCS11 code, we can just reuse the SPKI section from an existing certificate if we have it,
212+
/// i.e. we're renewing and not getting a brand new cert.
213+
///
214+
/// An alternative, looking at how it's done in gnutls (_pkcs11_privkey_get_pubkey and
215+
/// pkcs11_read_pubkey functions), seems to be:
216+
/// - if the key is RSA, a public key can be trivially derived from the public properties of PKCS11
217+
/// private key object
218+
/// - for EC key, it should also be possible to derive public from private
219+
/// - if that fails, a public key object may also be present on the token
220+
#[derive(Debug, Clone)]
221+
pub struct RemoteKeyPair {
222+
cryptoki_config: CryptokiConfig,
223+
public_key_raw: Vec<u8>,
224+
algorithm: &'static rcgen::SignatureAlgorithm,
225+
}
226+
227+
impl RemoteKeyPair {
228+
pub fn to_key_pair(&self) -> Result<KeyPair, CertificateError> {
229+
Ok(KeyPair::from_remote(Box::new(self.clone()))?)
230+
}
231+
}
232+
233+
impl rcgen::RemoteKeyPair for RemoteKeyPair {
234+
fn public_key(&self) -> &[u8] {
235+
&self.public_key_raw
236+
}
237+
238+
fn sign(&self, msg: &[u8]) -> Result<Vec<u8>, rcgen::Error> {
239+
// the error here is not PEM-related, but we need to return a foreign error type, and there
240+
// are no other better variants that could let us return context, so we'll have to use this
241+
// until `rcgen::Error::RemoteKeyError` can take a parameter
242+
let signer = tedge_p11_server::signing_key(self.cryptoki_config.clone())
243+
.map_err(|e| rcgen::Error::PemError(e.to_string()))?;
244+
signer
245+
.sign(msg)
246+
.map_err(|e| rcgen::Error::PemError(e.to_string()))
247+
}
248+
249+
fn algorithm(&self) -> &'static rcgen::SignatureAlgorithm {
250+
self.algorithm
251+
}
148252
}
149253

150254
pub struct KeyCertPair {
@@ -176,7 +280,9 @@ impl KeyCertPair {
176280
// as rcgen library will not parse it for certificate signing request
177281
let params = Self::create_csr_parameters(config, id, key_kind)?;
178282
Ok(KeyCertPair {
179-
certificate: Zeroizing::new(Certificate::from_params(params)?),
283+
certificate: Zeroizing::new(
284+
Certificate::from_params(params).context("Failed to create CSR")?,
285+
),
180286
})
181287
}
182288

@@ -215,15 +321,23 @@ impl KeyCertPair {
215321
let mut params = CertificateParams::default();
216322
params.distinguished_name = distinguished_name;
217323

218-
if let KeyKind::Reuse { keypair_pem } = key_kind {
219-
// Use the same signing algorithm as the existing key
220-
// Failing to do so leads to an error telling the algorithm is not compatible
221-
let key_pair = KeyPair::from_pem(keypair_pem)?;
222-
params.alg = key_pair.algorithm();
223-
params.key_pair = Some(key_pair);
224-
} else {
225-
// ECDSA signing using the P-256 curves and SHA-256 hashing as per RFC 5758
226-
params.alg = &rcgen::PKCS_ECDSA_P256_SHA256;
324+
match key_kind {
325+
KeyKind::New => {
326+
// ECDSA signing using the P-256 curves and SHA-256 hashing as per RFC 5758
327+
params.alg = &rcgen::PKCS_ECDSA_P256_SHA256;
328+
}
329+
KeyKind::Reuse { keypair_pem } => {
330+
// Use the same signing algorithm as the existing key
331+
// Failing to do so leads to an error telling the algorithm is not compatible
332+
let key_pair = KeyPair::from_pem(keypair_pem)?;
333+
params.alg = key_pair.algorithm();
334+
params.key_pair = Some(key_pair);
335+
}
336+
KeyKind::ReuseRemote(key_pair) => {
337+
let key_pair = key_pair.to_key_pair()?;
338+
params.alg = key_pair.algorithm();
339+
params.key_pair = Some(key_pair)
340+
}
227341
}
228342

229343
Ok(params)

crates/common/certificate/src/parse_root_certificate/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ pub fn create_tls_config_cryptoki(
4949

5050
let certified_key = CertifiedKey {
5151
cert: cert_chain,
52-
key,
52+
key: key.to_rustls_signing_key(),
5353
ocsp: None,
5454
};
5555
let resolver: SingleCertAndKey = certified_key.into();

crates/common/tedge_config/src/tedge_toml/tedge_config/mqtt_config.rs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ impl TEdgeConfig {
160160
cloud: &dyn CloudConfig,
161161
) -> anyhow::Result<MqttAuthConfigCloudBroker> {
162162
// if client cert is set, then either cryptoki or key file must be set
163-
let client_auth = match self.device.cryptoki_config(cloud)? {
163+
let client_auth = match self.device.cryptoki_config(Some(cloud))? {
164164
Some(cryptoki_config) => MqttAuthClientConfigCloudBroker {
165165
cert_file: cloud.device_cert_path().to_path_buf(),
166166
private_key: PrivateKeyType::Cryptoki(cryptoki_config),
@@ -213,12 +213,19 @@ impl TEdgeConfig {
213213
}
214214

215215
impl TEdgeConfigReaderDevice {
216+
/// Returns the cryptoki configuration.
217+
///
218+
/// - `Err` if config doesn't fit schema (e.g. set to module but module_path not set)
219+
/// - `Ok(None)` if mode set to `off`
220+
/// - `Ok(Some(CryptokiConfig))` for mode `socket` or `module`
216221
pub fn cryptoki_config(
217222
&self,
218-
cloud: &dyn CloudConfig,
223+
cloud: Option<&dyn CloudConfig>,
219224
) -> Result<Option<CryptokiConfig>, anyhow::Error> {
220225
let cryptoki = &self.cryptoki;
221-
let uri = cloud.key_uri().or(self.key_uri.or_none().cloned());
226+
let uri = cloud
227+
.and_then(|c| c.key_uri().or(self.key_uri.or_none().cloned()))
228+
.or(self.key_uri.or_none().cloned());
222229
match cryptoki.mode {
223230
Cryptoki::Off => Ok(None),
224231
Cryptoki::Module => Ok(Some(CryptokiConfig::Direct(CryptokiConfigDirect {

crates/core/tedge/src/cli/certificate/c8y/download.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use crate::cli::certificate::c8y::create_device_csr;
22
use crate::cli::certificate::c8y::read_csr_from_file;
33
use crate::cli::certificate::c8y::store_device_cert;
4+
use crate::cli::certificate::create_csr::Key;
45
use crate::cli::certificate::show::ShowCertCmd;
56
use crate::command::Command;
67
use crate::error;
@@ -41,7 +42,7 @@ pub struct DownloadCertCmd {
4142
pub cert_path: Utf8PathBuf,
4243

4344
/// The path where the device private key will be stored
44-
pub key_path: Utf8PathBuf,
45+
pub key: Key,
4546

4647
/// The path where the device CSR file will be stored
4748
pub csr_path: Utf8PathBuf,
@@ -83,7 +84,8 @@ impl DownloadCertCmd {
8384
if self.generate_csr {
8485
create_device_csr(
8586
common_name.clone(),
86-
self.key_path.clone(),
87+
self.key.clone(),
88+
None,
8789
self.csr_path.clone(),
8890
self.csr_template.clone(),
8991
)

crates/core/tedge/src/cli/certificate/c8y/mod.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,16 @@ pub use upload::UploadCertCmd;
1717
/// Return the CSR in the format expected by c8y CA
1818
async fn create_device_csr(
1919
common_name: String,
20-
key_path: Utf8PathBuf,
20+
key: super::create_csr::Key,
21+
current_cert: Option<Utf8PathBuf>,
2122
csr_path: Utf8PathBuf,
2223
csr_template: CsrTemplate,
2324
) -> Result<(), CertError> {
2425
let create_cmd = CreateCsrCmd {
2526
id: common_name,
2627
csr_path: csr_path.clone(),
27-
key_path,
28+
key,
29+
current_cert,
2830
user: "tedge".to_string(),
2931
group: "tedge".to_string(),
3032
csr_template,

crates/core/tedge/src/cli/certificate/c8y/renew.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use crate::certificate_cn;
22
use crate::cli::certificate::c8y::create_device_csr;
33
use crate::cli::certificate::c8y::read_csr_from_file;
44
use crate::cli::certificate::c8y::store_device_cert;
5+
use crate::cli::certificate::create_csr::Key;
56
use crate::cli::certificate::show::ShowCertCmd;
67
use crate::command::Command;
78
use crate::get_webpki_error_from_reqwest;
@@ -17,6 +18,8 @@ use hyper::StatusCode;
1718
use reqwest::Identity;
1819
use reqwest::Response;
1920
use tedge_config::TEdgeConfig;
21+
use tracing::debug;
22+
use tracing::instrument;
2023
use url::Url;
2124

2225
/// Command to renew a device certificate from Cumulocity
@@ -37,7 +40,7 @@ pub struct RenewCertCmd {
3740
pub new_cert_path: Utf8PathBuf,
3841

3942
/// The path of the private key to re-use
40-
pub key_path: Utf8PathBuf,
43+
pub key: Key,
4144

4245
/// The path where the device CSR file will be stored
4346
pub csr_path: Utf8PathBuf,
@@ -74,18 +77,21 @@ impl RenewCertCmd {
7477
self.c8y.get_base_url()
7578
}
7679

80+
#[instrument(skip_all)]
7781
async fn renew_device_certificate(&self) -> Result<(), Error> {
7882
if self.generate_csr {
7983
let common_name = certificate_cn(&self.cert_path).await?;
8084
create_device_csr(
8185
common_name,
82-
self.key_path.clone(),
86+
self.key.clone(),
87+
Some(self.cert_path.clone()),
8388
self.csr_path.clone(),
8489
self.csr_template.clone(),
8590
)
8691
.await?;
8792
}
8893
let csr = read_csr_from_file(&self.csr_path).await?;
94+
debug!(?self.csr_path, "Created CSR");
8995

9096
let http_builder = self.http_config.client_builder();
9197
let http_builder = if let Some(identity) = &self.identity {

0 commit comments

Comments
 (0)