Skip to content

Commit b5222c8

Browse files
committed
feat: tedge cert renew c8y w/ HSM
This commit introduces support for tedge cert renew c8y when the private key is located on the HSM. To generate a CSR, one needs to put the public key in the CSR (it will become a part of the certificate (SPKI)) and to sign the certificate using the private key to prove the ownership of it. Because the public key can be derived from the private key, and because we need to sign anyway, until this PR we just read the private key to do both these things. Now, with an HSM, we can't read the private key, so instead for CSR creation with HSM being enabled, we read the public key from the currently used certificate and sign using the TedgeP11Client. Signed-off-by: Marcel Guzik <marcel.guzik@cumulocity.com>
1 parent 80cdc31 commit b5222c8

File tree

13 files changed

+336
-56
lines changed

13 files changed

+336
-56
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)