Skip to content

Commit

Permalink
Merge pull request #2783 from Ruadhri17/certificate-signing-request
Browse files Browse the repository at this point in the history
feat(cert): Create a Certificate signing request using `tedge cert create-csr`
  • Loading branch information
Ruadhri17 authored Mar 22, 2024
2 parents 4109ef6 + 1395167 commit 6d3ae41
Show file tree
Hide file tree
Showing 12 changed files with 597 additions and 46 deletions.
137 changes: 123 additions & 14 deletions crates/common/certificate/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,15 +113,19 @@ impl KeyCertPair {
) -> Result<KeyCertPair, CertificateError> {
let today = OffsetDateTime::now_utc();
let not_before = today - Duration::days(1); // Ensure the certificate is valid today
KeyCertPair::new_selfsigned_certificate_at(config, id, not_before, key_kind)
let params =
Self::create_selfsigned_certificate_parameters(config, id, key_kind, not_before)?;
Ok(KeyCertPair {
certificate: Zeroizing::new(Certificate::from_params(params)?),
})
}

fn new_selfsigned_certificate_at(
fn create_selfsigned_certificate_parameters(
config: &NewCertificateConfig,
id: &str,
key_kind: &KeyKind,
not_before: OffsetDateTime,
cert_kind: &KeyKind,
) -> Result<KeyCertPair, CertificateError> {
) -> Result<CertificateParams, CertificateError> {
KeyCertPair::check_identifier(id, config.max_cn_size)?;
let mut distinguished_name = rcgen::DistinguishedName::new();
distinguished_name.push(rcgen::DnType::CommonName, id);
Expand All @@ -131,23 +135,64 @@ impl KeyCertPair {
&config.organizational_unit_name,
);

let not_after = not_before + Duration::days(config.validity_period_days.into());

let mut params = CertificateParams::default();

params.distinguished_name = distinguished_name;

let not_after = not_before + Duration::days(config.validity_period_days.into());
params.not_before = not_before;
params.not_after = not_after;
params.alg = &rcgen::PKCS_ECDSA_P256_SHA256; // ECDSA signing using the P-256 curves and SHA-256 hashing as per RFC 5758

params.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained); // IsCa::SelfSignedOnly is rejected by C8Y
if let KeyKind::Reuse { keypair_pem } = cert_kind {

params.alg = &rcgen::PKCS_ECDSA_P256_SHA256; // ECDSA signing using the P-256 curves and SHA-256 hashing as per RFC 5758

if let KeyKind::Reuse { keypair_pem } = key_kind {
params.key_pair = Some(KeyPair::from_pem(keypair_pem)?);
}

Ok(params)
}

// Create Certificate without `not_before` and `not_after` fields
// as rcgen library will not parse it for certificate signing request
pub fn new_certificate_sign_request(
config: &NewCertificateConfig,
id: &str,
key_kind: &KeyKind,
) -> Result<KeyCertPair, CertificateError> {
let params = Self::create_certificate_sign_request_parameters(config, id, key_kind)?;
Ok(KeyCertPair {
certificate: Zeroizing::new(Certificate::from_params(params)?),
})
}

fn create_certificate_sign_request_parameters(
config: &NewCertificateConfig,
id: &str,
key_kind: &KeyKind,
) -> Result<CertificateParams, CertificateError> {
KeyCertPair::check_identifier(id, config.max_cn_size)?;
let mut distinguished_name = rcgen::DistinguishedName::new();
distinguished_name.push(rcgen::DnType::CommonName, id);
distinguished_name.push(rcgen::DnType::OrganizationName, &config.organization_name);
distinguished_name.push(
rcgen::DnType::OrganizationalUnitName,
&config.organizational_unit_name,
);

let mut params = CertificateParams::default();
params.distinguished_name = distinguished_name;

params.alg = &rcgen::PKCS_ECDSA_P256_SHA256; // ECDSA signing using the P-256 curves and SHA-256 hashing as per RFC 5758

if let KeyKind::Reuse { keypair_pem } = key_kind {
params.key_pair = Some(KeyPair::from_pem(keypair_pem)?);
}

Ok(params)
}

pub fn certificate_pem_string(&self) -> Result<String, CertificateError> {
Ok(self.certificate.serialize_pem()?)
}
Expand All @@ -156,6 +201,10 @@ impl KeyCertPair {
Ok(Zeroizing::new(self.certificate.serialize_private_key_pem()))
}

pub fn certificate_signing_request_string(&self) -> Result<String, CertificateError> {
Ok(self.certificate.serialize_request_pem()?)
}

fn check_identifier(id: &str, max_cn_size: usize) -> Result<(), CertificateError> {
Ok(device_id::is_valid_device_id(id, max_cn_size)?)
}
Expand Down Expand Up @@ -243,6 +292,7 @@ mod tests {
use super::*;
use std::error::Error;
use time::macros::datetime;
use x509_parser::der_parser::asn1_rs::FromDer;

impl KeyCertPair {
fn new_selfsigned_certificate_with_new_key(
Expand All @@ -260,6 +310,24 @@ mod tests {
PemCertificate::from_pem_string(&pem_string).expect("Fail to decode the certificate PEM")
}

fn subject_of_csr(keypair: &KeyCertPair) -> String {
let csr = keypair
.certificate_signing_request_string()
.expect("Failed to read the CSR string");

let pem = x509_parser::pem::Pem::iter_from_buffer(csr.as_bytes())
.next()
.unwrap()
.expect("Reading PEM block failed");

x509_parser::certification_request::X509CertificationRequest::from_der(&pem.contents)
.unwrap()
.1
.certification_request_info
.subject
.to_string()
}

#[test]
fn self_signed_cert_subject_is_the_device() {
// Create a certificate with a given subject
Expand Down Expand Up @@ -326,9 +394,19 @@ mod tests {
let id = "some-id";
let birthdate = datetime!(2021-03-31 16:39:57 +01:00);

let keypair =
KeyCertPair::new_selfsigned_certificate_at(&config, id, birthdate, &KeyKind::New)
.expect("Fail to create a certificate");
let params = KeyCertPair::create_selfsigned_certificate_parameters(
&config,
id,
&KeyKind::New,
birthdate,
)
.expect("Fail to get a certificate parameters");

let keypair = KeyCertPair {
certificate: Zeroizing::new(
Certificate::from_params(params).expect("Fail to create a certificate"),
),
};

// Check the not_before date
let pem = pem_of_keypair(&keypair);
Expand All @@ -348,16 +426,47 @@ mod tests {
let id = "some-id";
let birthdate = datetime!(2021-03-31 16:39:57 +01:00);

let keypair =
KeyCertPair::new_selfsigned_certificate_at(&config, id, birthdate, &KeyKind::New)
.expect("Fail to create a certificate");
let params = KeyCertPair::create_selfsigned_certificate_parameters(
&config,
id,
&KeyKind::New,
birthdate,
)
.expect("Fail to get a certificate parameters");

let keypair = KeyCertPair {
certificate: Zeroizing::new(
Certificate::from_params(params).expect("Fail to create a certificate"),
),
};

// Check the not_after date
let pem = pem_of_keypair(&keypair);
let not_after = pem.not_after().expect("Fail to extract the not_after date");
assert_eq!(not_after, "Sat, 10 Apr 2021 15:39:57 +0000");
}

#[test]
fn create_certificate_sign_request() {
// Create a certificate with a given birthdate.
let config = NewCertificateConfig::default();
let id = "some-id";

let params =
KeyCertPair::create_certificate_sign_request_parameters(&config, id, &KeyKind::New)
.expect("Fail to get a certificate parameters");

let keypair = KeyCertPair {
certificate: Zeroizing::new(
Certificate::from_params(params).expect("Fail to create a certificate"),
),
};

// Check the subject
let subject = subject_of_csr(&keypair);
assert_eq!(subject, "CN=some-id, O=Thin Edge, OU=Test Device");
}

#[test]
fn check_certificate_thumbprint_b64_decode_sha1() {
// Create a certificate key pair
Expand Down
12 changes: 12 additions & 0 deletions crates/common/tedge_config/src/tedge_config_cli/tedge_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,11 @@ define_tedge_config! {
#[doku(as = "PathBuf")]
cert_path: Utf8PathBuf,

/// Path where the device's certificate signing request is stored
#[tedge_config(example = "/etc/tedge/device-certs/tedge.csr", default(function = "default_device_csr"))]
#[doku(as = "PathBuf")]
csr_path: Utf8PathBuf,

/// The default device type
#[tedge_config(example = "thin-edge.io", default(value = "thin-edge.io"))]
#[tedge_config(rename = "type")]
Expand Down Expand Up @@ -954,6 +959,13 @@ fn default_device_cert(location: &TEdgeConfigLocation) -> Utf8PathBuf {
.join("tedge-certificate.pem")
}

fn default_device_csr(location: &TEdgeConfigLocation) -> Utf8PathBuf {
location
.tedge_config_root_path()
.join("device-certs")
.join("tedge.csr")
}

fn default_mqtt_port() -> NonZeroU16 {
NonZeroU16::try_from(1883).unwrap()
}
Expand Down
14 changes: 14 additions & 0 deletions crates/common/tedge_utils/src/file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,20 @@ pub fn create_file_with_mode(
perm_entry.create_file(file.as_ref(), content)
}

pub fn create_file_with_mode_or_overwrite(
file: impl AsRef<Path>,
content: Option<&str>,
mode: u32,
) -> Result<(), FileError> {
match content {
Some(content) if file.as_ref().exists() => overwrite_file(file.as_ref(), content),
_ => {
let perm_entry = PermissionEntry::new(None, None, Some(mode));
perm_entry.create_file(file.as_ref(), content)
}
}
}

pub fn create_file_with_user_group(
file: impl AsRef<Path>,
user: &str,
Expand Down
25 changes: 25 additions & 0 deletions crates/core/tedge/src/cli/certificate/cli.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use camino::Utf8PathBuf;
use tedge_config::OptionalConfigError;

use super::create::CreateCertCmd;
use super::create_csr::CreateCsrCmd;
use super::remove::RemoveCertCmd;
use super::renew::RenewCertCmd;
use super::show::ShowCertCmd;
Expand All @@ -20,6 +22,17 @@ pub enum TEdgeCertCli {
id: String,
},

/// Create a certificate signing request
CreateCsr {
/// The device identifier to be used as the common name for the certificate
#[clap(long = "device-id")]
id: Option<String>,

/// Path where a Certificate signing request will be stored
#[clap(long = "output-path")]
output_path: Option<Utf8PathBuf>,
},

/// Renew the device certificate
Renew,

Expand All @@ -44,6 +57,18 @@ impl BuildCommand for TEdgeCertCli {
id,
cert_path: config.device.cert_path.clone(),
key_path: config.device.key_path.clone(),
csr_path: None,
};
cmd.into_boxed()
}

TEdgeCertCli::CreateCsr { id, output_path } => {
let cmd = CreateCsrCmd {
id,
cert_path: config.device.cert_path.clone(),
key_path: config.device.key_path.clone(),
// Use output file instead of csr_path from tedge config if provided
csr_path: output_path.unwrap_or_else(|| config.device.csr_path.clone()),
};
cmd.into_boxed()
}
Expand Down
Loading

0 comments on commit 6d3ae41

Please sign in to comment.