parsec_tool/subcommands/
create_csr.rs

1// Copyright 2021 Contributors to the Parsec project.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Creates a Certificate Signing Request (CSR) from a keypair.
5
6use crate::error::{Error, Result, ToolErrorKind};
7use crate::util::sign_message_with_policy;
8use log::error;
9use parsec_client::core::interface::operations::psa_algorithm::{
10    Algorithm, AsymmetricSignature, Hash, SignHash,
11};
12use parsec_client::core::interface::operations::psa_key_attributes::{EccFamily, Type};
13use parsec_client::BasicClient;
14use rcgen::{
15    Certificate, CertificateParams, DistinguishedName, DnType, KeyPair, RcgenError, RemoteKeyPair,
16    SignatureAlgorithm, PKCS_ECDSA_P256_SHA256, PKCS_ECDSA_P384_SHA384, PKCS_RSA_SHA256,
17    PKCS_RSA_SHA384, PKCS_RSA_SHA512,
18};
19use structopt::StructOpt;
20
21/// Creates an X509 Certificate Signing Request (CSR) from a keypair, using the signing algorithm
22/// that is associated with the key.
23///
24/// The CSR is written to the standard output in PEM format by default.
25#[derive(Debug, StructOpt)]
26pub struct CreateCsr {
27    /// The name of the key to use for signing. This must be an existing key that is accessible
28    /// to the user, and it must be a signing key (either an RSA key or an elliptic curve key).
29    ///
30    /// Elliptic curve keys must use the NIST P256 or P384 curves.
31    #[structopt(short = "k", long = "key-name")]
32    key_name: String,
33
34    /// The common name to be used within the Distinguished Name (DN) specification of
35    /// the CSR.
36    #[structopt(long = "cn")]
37    common_name: Option<String>,
38
39    /// The locality name to be used within the Distinguished Name (DN) specification of
40    /// the CSR.
41    #[structopt(long = "l")]
42    locality: Option<String>,
43
44    /// The organization name to be used within the Distinguished Name (DN) specification of
45    /// the CSR.
46    #[structopt(long = "o")]
47    organization: Option<String>,
48
49    /// The organizational unit name to be used within the Distinguished Name (DN) specification
50    /// of the CSR.
51    #[structopt(long = "ou")]
52    organizational_unit: Option<String>,
53
54    /// The state name to be used within the Distinguished Name (DN) specification of the CSR.
55    #[structopt(long = "st")]
56    state: Option<String>,
57
58    /// The country name to be used within the Distinguished Name (DN) specification of the CSR.
59    #[structopt(long = "c")]
60    country: Option<String>,
61
62    /// The serial number to be used within the Distinguished Name (DN) specification of the CSR.
63    #[structopt(long = "serialNumber")]
64    serial_number: Option<String>,
65
66    /// A Subject Alternative Name (SAN) for the domain of the CSR.
67    #[structopt(long = "san")]
68    subject_alternative_name: Option<Vec<String>>,
69}
70
71/// Short-lived structure to encapsulate the key name and the client, so that we can implement the
72/// RemoteKeyPair trait for rcgen.
73struct ParsecRemoteKeyPair {
74    key_name: String,
75    public_key_der: Vec<u8>,
76    parsec_client: BasicClient,
77    rcgen_algorithm: &'static SignatureAlgorithm,
78}
79
80impl CreateCsr {
81    /// Creates a Certificate Signing Request (CSR) from a keypair.
82    pub fn run(&self, basic_client: BasicClient) -> Result<()> {
83        let public_key = basic_client.psa_export_public_key(&self.key_name)?;
84
85        let rcgen_algorithm = self.get_rcgen_algorithm(&basic_client)?;
86
87        let parsec_key_pair = ParsecRemoteKeyPair {
88            key_name: self.key_name.clone(),
89            public_key_der: public_key,
90            // "Move" the client into the struct here.
91            parsec_client: basic_client,
92            rcgen_algorithm,
93        };
94
95        let remote_key_pair = KeyPair::from_remote(Box::new(parsec_key_pair))?;
96
97        let subject_alt_names = match &self.subject_alternative_name {
98            Some(san) => san.to_owned(),
99            None => Vec::new(),
100        };
101
102        let mut dn = DistinguishedName::new();
103
104        if let Some(common_name) = &self.common_name {
105            dn.push(DnType::CommonName, common_name.clone());
106        }
107
108        if let Some(organizational_unit) = &self.organizational_unit {
109            // NOTE: X509 permits multiple OUs, but the RCGEN crate only preserves one entry, so for now the
110            // parsec-tool also only accepts one entry on the command-line. If this changes in the future, it
111            // will be possible to evolve the command-line parser to accept multiple values without it being
112            // a breaking change.
113            dn.push(DnType::OrganizationalUnitName, organizational_unit.clone());
114        }
115
116        if let Some(organization) = &self.organization {
117            dn.push(DnType::OrganizationName, organization.clone());
118        }
119
120        if let Some(locality) = &self.locality {
121            dn.push(DnType::LocalityName, locality.clone());
122        }
123
124        if let Some(state) = &self.state {
125            dn.push(DnType::StateOrProvinceName, state.clone());
126        }
127
128        if let Some(country) = &self.country {
129            dn.push(DnType::CountryName, country.clone());
130        }
131
132        if let Some(serial_number) = &self.serial_number {
133            // Rcgen does not have a DnType::SerialNumber, so use DnType::CustomDnType and supply the
134            // Object ID (OID) numerically. The OID for X509 serialNumber is 2.5.4.5 according to
135            // https://www.alvestrand.no/objectid/2.5.4.5.html and other sources.
136            dn.push(
137                DnType::CustomDnType(vec![2, 5, 4, 5]),
138                serial_number.clone(),
139            );
140        }
141
142        let mut params = CertificateParams::new(subject_alt_names);
143        params.alg = rcgen_algorithm;
144        params.key_pair = Some(remote_key_pair);
145        params.distinguished_name = dn;
146
147        let cert = Certificate::from_params(params)?;
148
149        let pem_string = cert.serialize_request_pem()?;
150
151        println!("{}", pem_string);
152
153        Ok(())
154    }
155
156    // Inspect the attributes of the signing key and map them down to one of rcgen's supported hash-and-sign
157    // schemes (throwing an error if there isn't a suitable mapping).
158    //
159    // There's rather a lot of complexity here, because we need to map down lots of nested PSA properties onto a small number
160    // of hash-and-sign schemes that RCGEN supports.
161    fn get_rcgen_algorithm(
162        &self,
163        basic_client: &BasicClient,
164    ) -> Result<&'static SignatureAlgorithm> {
165        let attributes = basic_client.key_attributes(&self.key_name)?;
166
167        if let Algorithm::AsymmetricSignature(alg) = attributes.policy.permitted_algorithms {
168            match alg {
169                AsymmetricSignature::RsaPkcs1v15Sign { hash_alg } => match hash_alg {
170                    SignHash::Specific(Hash::Sha256) => Ok(&PKCS_RSA_SHA256),
171                    SignHash::Specific(Hash::Sha384) => Ok(&PKCS_RSA_SHA384),
172                    SignHash::Specific(Hash::Sha512) => Ok(&PKCS_RSA_SHA512),
173                    SignHash::Any => Ok(&PKCS_RSA_SHA256), // Default hash algorithm for the tool.
174                    _ => {
175                        // The algorithm is specific, but not one that RCGEN can use, so fail the operation.
176                        error!("Signing key requires use of hashing algorithm ({:?}), which is not supported for certificate requests.", alg);
177                        Err(ToolErrorKind::NotSupported.into())
178                    }
179                },
180                AsymmetricSignature::RsaPkcs1v15SignRaw => {
181                    // Key policy specifies raw RSA signatures. RCGEN will always hash-and-sign, so fail.
182                    error!("Signing key specifies raw signing only, which is not supported for certificate requests.");
183                    Err(ToolErrorKind::NotSupported.into())
184                }
185                AsymmetricSignature::RsaPss { .. } => {
186                    error!("Signing key specifies RSA PSS scheme, which is not supported for certificate requests.");
187                    Err(ToolErrorKind::NotSupported.into())
188                }
189                AsymmetricSignature::Ecdsa { hash_alg } => {
190                    if !matches!(
191                        attributes.key_type,
192                        Type::EccKeyPair {
193                            curve_family: EccFamily::SecpR1
194                        }
195                    ) {
196                        error!(
197                            "Signing key must use curve family SecpR1 for certificate requests."
198                        );
199                        return Err(ToolErrorKind::NotSupported.into());
200                    };
201
202                    match hash_alg {
203                        SignHash::Specific(Hash::Sha256) => {
204                            if attributes.bits == 256 {
205                                Ok(&PKCS_ECDSA_P256_SHA256)
206                            } else {
207                                error!("Signing key should have strength 256, but actually has strength {}.", attributes.bits);
208                                Err(ToolErrorKind::NotSupported.into())
209                            }
210                        }
211                        SignHash::Specific(Hash::Sha384) => {
212                            if attributes.bits == 384 {
213                                Ok(&PKCS_ECDSA_P384_SHA384)
214                            } else {
215                                error!("Signing key should have strength 384, but actually has strength {}.", attributes.bits);
216                                Err(ToolErrorKind::NotSupported.into())
217                            }
218                        }
219                        SignHash::Any => {
220                            match attributes.bits {
221                                256 => Ok(&PKCS_ECDSA_P256_SHA256),
222                                _ => {
223                                    // We have to fail this, because ParsecRemoteKeyPair::sign() defaults the hash to SHA-256, and RCGEN
224                                    // doesn't support a hash algorithm that is different from the key strength.
225                                    error!("Signing keys of strength other than 256-bit not supported without specific hash algorithm.");
226                                    Err(ToolErrorKind::NotSupported.into())
227                                }
228                            }
229                        }
230                        _ => {
231                            // The algorithm is specific, but not one that RCGEN can use, so fail the operation.
232                            error!("Signing key requires use of hashing algorithm ({:?}), which is not supported for certificate requests.", alg);
233                            Err(ToolErrorKind::NotSupported.into())
234                        }
235                    }
236                }
237                _ => {
238                    // Unsupported algorithm.
239                    error!("The specified key is not supported for certificate requests.");
240                    Err(ToolErrorKind::NotSupported.into())
241                }
242            }
243        } else {
244            error!("Specified key is not an asymmetric signing key, which is needed for certificate requests.");
245            Err(ToolErrorKind::WrongKeyAlgorithm.into())
246        }
247    }
248}
249
250impl RemoteKeyPair for ParsecRemoteKeyPair {
251    fn public_key(&self) -> &[u8] {
252        &self.public_key_der
253    }
254
255    fn sign(&self, msg: &[u8]) -> std::result::Result<Vec<u8>, RcgenError> {
256        let signature =
257            sign_message_with_policy(&self.parsec_client, &self.key_name, msg, Some(Hash::Sha256))
258                .map_err(RcgenError::from)?;
259        Ok(signature)
260    }
261
262    fn algorithm(&self) -> &'static SignatureAlgorithm {
263        self.rcgen_algorithm
264    }
265}
266
267impl From<Error> for RcgenError {
268    fn from(_e: Error) -> Self {
269        // There isn't a suitable mapping, because RcgenError does not have a variant for the
270        // case where RemoteKeyPair failed for third-party reasons.
271        // See: https://github.com/est31/rcgen/issues/67
272        // The crate will publish a new enum variant. When this change is released, we can rework this to be a
273        // more suitable error.
274        RcgenError::KeyGenerationUnavailable
275    }
276}